1日1Zenn
ステータス
App Previewの仕様
スクリーンショットの仕様
Apple Developer向けのリソース
Info.plist
のキー
AppStore Review Guidelinesのヒストリー
Swift Evolution / enableUpcomingFeatureの検索
StoreKitのテストシナリオ
Photoshopで白抜きする方法
NSFontで太字を使いたい
var font = NSFont(name: "フォント名", size: 18.0)!
font = NSFontManager.shared.convertWeight(true, of: font)
これでBoldになるっぽい
等幅のフリーフォント
自作のmacOS向けライブラリを Swift Package Manager 対応する件
何が正しいのかいまいちわからない。
- https://qiita.com/jjjkkkjjj/items/727517263292ae7a3a87
- 自作のパッケージをCocoaPods、Carthage、Swift Package Manager対応させる
- How can I migrate an xcassets catalog to Swift Package Manager?
画像リソースはBundle.module.image(forResource: NAME)
で取得するみたい。
ヒントっぽい記事
すごい記事を見つけた。
Macで最優先のネットワークインターフェイスの取得方法
URLからファイル名だけを取得する方法(拡張子を除く)
import Foundation
extension URL {
var fileName: String {
return self.deletingPathExtension().lastPathComponent
}
}
let url = URL(fileURLWithPath: "/Users/takuto/Desktop/EpG-3qSU8AAaU-x.jpg")
print(url.fileName)
EpG-3qSU8AAaU-x
環境設定 -> セキュリティーとプライバシー -> プライバシーの各項目に飛ぶ方法
let path = "x-apple.systempreferences:com.apple.preference.security?キー"
NSWorkspace.shared.open(URL(string: path)!)
キー一覧
Catalina以降でBluetoothのプライバシー設定がどうなっているか確認する方法
import CoreBluetooth
if #available(OSX 10.15, *) {
switch CBCentralManager.authorization {
case .notDetermined:
logput("未設定")
case .restricted:
logput("制限あり(どうしたらこうなるのか不明)")
case .denied:
logput("許可されていない(チェックが外れている)")
case .allowedAlways:
logput("許可されている(チェックがついている)")
@unknown default:
logput("default")
}
}
Catalina以降でScreenRecordingのパーミッションの確認
func allowedScreenRecording() -> Bool {
return CGWindowListCreateImage(
CGRect(x: 0, y: 0, width: 1, height: 1),
CGWindowListOption.optionOnScreenOnly,
kCGNullWindowID,
[]
) != nil
}
これを使うと初回時は許可のモーダルが出る。
AVFoundationのAVCaptureDevice.authorizationStatus(for: AVMediaType)
ではScreenRecordingのパーミッションについては取得できなかった。
Bluetooth構成(Bluetooth Configuration)を使ってBluetooth MIDI キーボードをつなぎ、QuickMIDIの対象として接続を開始した後にデバイス側からBluetooth接続をぶつ切りすると、アプリが死ぬパターンと死なないパターンがある。
Bluetooth構成の画面を開きっぱなしだと死なないのに、Bluetooth構成を閉じると死ぬ。
⇨CABTLEMIDIWindowController()を破棄せずに持つようにしたら死ななくなった...
メモリ的にちょっとやだ。
CAInterDeviceAudioViewControllerってなんぞ
おー、CABTLEMIDIWindowController()をずっと保持するようにして使ってみたら、Bluetooth構成が明らかにおかしい感じなことがわかったぞ!MIDIキーボードの方はもう接続を切っているのに、Bluetooth構成では接続解除ボタンがまだ押せる状態で表示されてしまう。なんだこれ。
App Store Connect API を Node.jsで使う
Javascript のasync/awaitについて
NSApp.orderFrontCharacterPalette(nil)
で「絵文字と記号」のパレットを開くときのコツ
NSTextViewが設置してあるウィンドウでNSButtonなどをトリガーにして開くのは無理。2回開いた後に3回目が開けなくなる(謎)。何度でも機能するためにはNSMenu経由で開いた方がいい。
未保存のファイルがあるのに終了しようとした時に伺うやつ
Xcodeプロジェクトをgit管理する時の.gitignore
必須項目
# macだと勝手に発生するいらないファイル
.DS_Store
# Xcodeでプロジェクトを開いていた状態の記録(ソースを変更していなくても状態が変わるだけで差分が出てしまう)
*.xcuserstate #
.gitignore
を更新した時にすでに差分管理されてしまっているファイルを除外する方法
$ git rm -r --cached .
をしてからadd commit pushすればいいだけ
macOS アプリのアプリ表示名を変更する方法
新規プロジェクトを作る時にSampleProject
みたいな名前にすると、そのままSampleProject.app
という名前のアプリになると思う。そこをSample Project.app
のようにスペースを入れたい場合など、表示名を変更したい場合の対処。
⭐️ Targetsでアプリのターゲットを選んで、Build SettingsでProduct Name
を探してその値を変更する。
- PROJECTの方の
Product Name
を変更しても効果はない。 - iOSでは効果があるInfo.plistの
Bundle display name
を変更しても効果はない。
MIDI の仕様の勉強
Windowsでスクショ
改良
- https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURL
- filter: brightness(0) invert() brightness(0.462);
OSStatusについて調べられるサイト
WindowsにはNotifyIconだけじゃなく、DeskBandsというのがある
- https://stackoverflow.com/questions/4028636/how-can-i-track-the-progress-of-avassetwriter-s-writing
- https://medium.com/samkirkiles/swift-using-avassetwriter-to-compress-video-files-for-network-transfer-4dcc7b4288c5
- https://blog.foresta.me/posts/swift_hls_download_bug/
- https://aviutl.info/ko-dekku-tigai/#toc2
- https://gist.github.com/jaumevn/9ba329aaf49c81c57a276fd135f53f20
MIDIUniqueID
はmac端末ごとには別のIDが割り振られるが、
アプリを再起動しても、クリーンビルドしても同じIDが得られる。
macを再起動しても、同じIDが得られる。
USBのポートを切り替えても同じIDが得られる。
Bluetoothと有線の両方の機能を持っているMIDIキーボードの場合は、それぞれ別のIDを持っているが、切り替えてもIDそのものは変わらない。
同じ端末で、別のIDが割り振られるパターンがあるのか気になる。
=>とりあえず、不具合報告がくるまでは信用して良さそう。
node の path でホームディレクトリを取得する
const path = require("path");
path.resolve(process.env.HOME);
NSColorWell で 透明度のスライダーを表示させる方法
override func awakeFromNib() {
super.awakeFromNib()
NSColor.ignoresAlpha = false
}
HandyPaletteの色
UI Element Colors
NSButtonのクリック挙動
デフォルトだと、mouseDownはキャッチしているが、mouseUpはキャッチしていない。
むしろrightMouseDownとrightMouseUpはキャッチしている。ただし、右クリックの時はボタンのハイライトは変更されないし、クリックしてもコーディングしなければボタンを押したときのイベントは流れてこない。
左クリックしている間は他のUIの更新も止まる!
NSStatusBarButtonのクリック挙動
- デフォルトではmouseDownとrightMouseUpがコールされる
- mouseDownをoverrideして中で何も処理させないようにすると、mouseUpをコールするようになる。一方で
.sendAction(on: [.leftMouseDown, .leftMouseUp])
していてもactionはコールされなくなる
Main.storyboardのメニューバーを削除した場合、TextFieldはどうなるのか&対策
- control + キー 系の操作は生きたまま
- ctrl + a: 先頭
- ctrl + t: 入替
- ctrl + e: 末尾
- shift + ctrl + e: 末尾まで選択
- ctrl + d: 右削除
- ctrl + k: 右全部削除
- ctrl + f/b/n/p カーソル移動
- command + キー系の操作は死んだ
- cmd + z: undo
- shift + cmd + z: redo
- cmd + x: カット
- cmd + c: コピー
- cmd + v: ペースト
- cmd + a: すべてを選択
対策
以下のNSTextFieldのextensionを書けば、NSTextFieldもNSTextViewも対応できる。
import Cocoa
extension NSTextField {
open override func performKeyEquivalent(with event: NSEvent) -> Bool {
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
if flags == [.command] {
let selector: Selector
switch event.charactersIgnoringModifiers?.lowercased() {
case "x": selector = #selector(NSText.cut(_:))
case "c": selector = #selector(NSText.copy(_:))
case "v": selector = #selector(NSText.paste(_:))
case "a": selector = #selector(NSText.selectAll(_:))
case "z": selector = Selector(("undo:"))
default: return super.performKeyEquivalent(with: event)
}
return NSApp.sendAction(selector, to: nil, from: self)
} else if flags == [.shift, .command] {
if event.charactersIgnoringModifiers?.lowercased() == "z" {
return NSApp.sendAction(Selector(("redo:")), to: nil, from: self)
}
self.undoManager?.undo()
}
return super.performKeyEquivalent(with: event)
}
}
NSTextViewの場合は、StoryboardかコードでUndoの許可設定をする必要がある。
参考
AppDelegateについて
Dockのアイコン長押しでメニューを開くやつ
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
// dock にメニューを追加できるよ
}
アプリケーションの起動と同時にファイルを開く
// ⭐️
// macOS 10.13以降
func application(_ application: NSApplication, open urls: [URL]) {
}
// ⭐️が実装されていると呼ばれない
// macOS 10.0以降
// applicationWillFinishLaunching(_:) より後にコールされる
// applicationDidFinishLaunching(_:) より前にコールされる
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
}
// ⭐️が実装されていると呼ばれない
// macOS 10.3以降
func application(_ sender: NSApplication, openFiles filenames: [String]) {
}
NSImageView で大きいサイズの画像を読み込んだ時にAutoLayoutが暴走しない方法
1.NSImageView.imageを使う方法
- NSImageViewをstoryboard/xibで選択して、Size Inspectorを開く
- Content Compression Resistance Priorityを450に設定する
imageView.imageScaling = .scaleProportionallyUpOrDown
imageView.imageAlignment = .alignCenter
imageView.image = NSImage()
2.NSImageView.layer.contentsを使う方法
imageView.wantsLayer = true
imageView.layer?.contentsGravity = CALayerContentsGravity.resizeAspect
imageView.layer?.contents = NSImage()
- 画像の大きさオーバーフローを防ぐやつ https://developer.apple.com/forums/thread/106782
- flippedのやつ https://stackoverflow.com/a/55101488
NSImageからPixel/inchを取得する方法
extension NSImage {
var ppi: CGFloat {
let rep = self.representations[0]
return 72.0 * CGFloat(rep.pixelsWide) / self.size.width
}
}
enum のケース名を出力する
enum Gomi {
case hoge
case meu
}
let hoge = Gomi.hoge
print(String(describing: hoge))
print(String(reflecting: hoge))
GitHub Actions のワークフローはトリガーがworkflow_dispatch
の場合はデフォルトブランチでないと認識されない。
AWSのEC2では、/usr/share/vim/vimrc/
のなかに.vimrc
をおけばどこでも.vimrc
が効くようになる。
Homebrewでmariadbをインストールした場合のコマンドの叩き方
before | after |
---|---|
mysql.server start | brew services start mariadb |
mysql.server stop | brew services stop mariadb |
brew services list |
SwiftUI で MVVM
要素 | 型 |
---|---|
View | struct, View |
ViewModel | class, ObservableObject |
Model | protocol & struct |
だいたいの流れ
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: ViewModel(model: MyModel()))
}
}
}
protocol Model {
var value1: Int { get }
var value2: Int { get set }
mutating func method()
}
struct MyModel: Model {
let value1: Int = 0
var value2: Int = 0
mutating func method() {
value2 += 1
}
}
@MainActor
class ViewModel: ObservableObject {
@Published var model: Model
var value1: Int {
return model.value1
}
func method() {
model.method()
}
}
struct ContentView: View {
@StateObject var viewModel: ViewModel
var body: some View {
Text("hello \(viewModel.value1)")
SubView(viewModel).padding(16)
}
}
struct SubView: View {
@ObservedObject var viewModel: ViewModel
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
Button("Push me") {
viewModel.method()
}
}
}
オブジェクトをPrettyPrint
func pretty(_ object: Any) {
guard let data = try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted),
let str = String(data: data, encoding: .utf8)
else {
Swift.print("Cannnot serialize the object.")
return
}
Swift.print(str)
}
Android Studio のJDKをJAVA_HOMEに指定する方法
Android Studio でプロジェクトを開いて、Preferences -> Build, Execution, Deployment -> Build Tools -> Gradleでパスが確認できる。
例えば、/Applications/Android Studio.app/Contents/jre/Contents/Home
これを.zprofile
に書き込めばいい。
# Java
export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"
$ java -version
openjdk version "11.0.11" 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+0-b60-7772763)
OpenJDK 64-Bit Server VM (build 11.0.11+0-b60-7772763, mixed mode)
$
SwiftUIで自作したSF Symbolsを扱うときのTips
普通にImage()
で初期化して読み込むとTabView
の.tabItem
においてアイコンとラベルの間のマージンが考慮されないしボケてしまう。こういうときは一旦NSImage()
で読み込んでおいてそれをImage()
に渡すといい。
悪い例
いい例
import SwiftUI
struct SettingsView: View {
private enum Tabs: Hashable {
case general
case format
}
var body: some View {
TabView {
GeneralSettingsView()
.tabItem {
Label("general", systemImage: "gear")
}
.tag(Tabs.general)
FormatSettingsView()
.tabItem {
Label {
Text("format")
} icon: {
// copy は自作したSymbol (SVG)
- Image("copy")
+ Image(nsImage: NSImage(named: "copy")!)
}
}
.tag(Tabs.format)
}
.padding(.horizontal, 40)
.padding(.vertical, 20)
}
}
Swift Package のexecutableでUniversal Binaryを作る
M1 (arm64) と Intel (x86_64) の両方で動く実行ファイルを作りたい。
swift build -c release --product [Product Name] --arch arm64 --arch x86_64
.build/apple/Products/Release
に生成される。
SwiftUI Ventura でListを使うときにキー入力での意図しない行選択を無効にする
SwiftUIのList
の内部実装はNSOutlineView
でできている。そのallowsTypeSelect
プロパティをfalse
にしてやれば、表題の意図しない操作を無効にできる。
extension NSOutlineView {
open override var allowsTypeSelect: Bool {
get { return false }
set {}
}
}
SwiftUIで鍵盤作る
まずは白鍵と黒鍵のShapeを用意
import SwiftUI
enum KeyType {
case white
case black
}
struct KeyShape: Shape {
var keyType: KeyType
func path(in rect: CGRect) -> Path {
let w = rect.size.width
let h = rect.size.height
let r = w / 6.0
let d = w / (keyType == .white ? 20.0 : 10.0)
let l: CGFloat = (keyType == .white ? 1.0 : 0.6)
var path = Path()
path.move(to: CGPoint(x: d, y: 0))
path.addLine(to: CGPoint(x: w - d, y: 0))
path.addLine(to: CGPoint(x: w - d, y: l * h - r))
path.addArc(center: CGPoint(x: w - d - r, y: l * h - r),
radius: r,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 90),
clockwise: false)
path.addLine(to: CGPoint(x: d + r, y: l * h))
path.addArc(center: CGPoint(x: d + r, y: l * h - r),
radius: r,
startAngle: Angle(degrees: 90),
endAngle: Angle(degrees: 180),
clockwise: false)
path.addLine(to: CGPoint(x: d, y: 0))
path.closeSubpath()
return path
}
}
そしたらいい感じに並べる
struct KeyboardView<KVM: KeyboardViewModel>: View {
var body: some View {
ZStack(alignment: .top) {
// White Keys
HStack(spacing: 0) {
ForEach(0 ..< 15, id: \.self) { _ in
KeyShape(keyType: .white)
.fill(Color.white)
.aspectRatio(0.14, contentMode: .fit)
}
}
// Black Keys
HStack(spacing: 0) {
ForEach(0 ..< 14, id: \.self) { i in
KeyShape(keyType: .black)
.fill(Color.black)
.aspectRatio(0.14, contentMode: .fit)
.opacity(i % 7 == 2 || i % 7 == 6 ? 0.0 : 1.0)
}
}
}
}
}
黒鍵の歯抜けのところは.opacity()
でうまいことやる。(.hidden()
だと条件分岐しずらいので)
常駐型アプリでキーボードしか使えなくなった時の対処法
-
command + Tab
でXcodeを選択 -
command + .
でXcodeの実行を止める - SpotlightでActivity Monitor.appを開く
-
option + command + F
で検索 -> 名前を絞る -
command + A
で全選択 -
option + command + Q
で終了する
メニューバーの高さを取得する方法
if let screen = NSScreen.main {
let height = screen.frame.height - screen.visibleFrame.height
}
Live ActivityでRunCat走らせる
func cat(_ date: Date, _ size: CGFloat) -> some View {
Group {
Group {
Text(date, style: .timer)
.font(.custom("RunningCat-Regular", size: size))
.lineLimit(1)
.truncationMode(.head)
.frame(width: 1.8 * size)
.contentTransition(.identity)
}
}
.frame(width: 1.74 * size,
height: 0.55 * size,
alignment: .leadingFirstTextBaseline)
.clipped()
.frame(width: 0.87 * size, alignment: .trailing)
.clipped()
}
SwiftPMのキャッシュを消す
-
~/Library/Caches/org.swift.swiftpm
にある該当パッケージの情報 -
~/Library/org.swift.swiftpm
にある該当パッケージの情報 -
~/Library/org.swift.swiftpm.lock
にある該当パッケージの情報 -
DerrivedData
のSourcePackages
- ローカルパッケージ内の
.swiftpm
SwiftでフォルダをZip化
// containerURL はZipしたいフォルダ
let coordinator = NSFileCoordinator()
var error: NSError?
var zipURL: URL?
coordinator.coordinate(readingItemAt: containerURL, options: .forUploading, error: &error) {
do {
let tmpURL = containerURL.appendingPathExtension(for: .zip)
try FileManager.default.moveItem(at: $0, to: tmpURL)
zipURL = tmpURL
} catch {
logput(error.localizedDescription)
}
}
guard error == nil, let zipURL else { throw CocoaError(.fileNoSuchFile) }
fileExporterの使い方
-
URL
を移動をする場合、Transferable
を使うstruct ExportFile: Transferable { let exportHandler: () throws -> URL static var transferRepresentation: some TransferRepresentation { FileRepresentation(exportedContentType: .png) { exportFile in let cacheFileURL = try exportFile.exportHandler() return SentTransferredFile(cacheFileURL) } } }
.fileExporter( isPresented: $viewModel.isPresented, item: ExportFile(exportHandler: { try viewModel.generateImage() }), contentTypes: [.png], defaultFilename: "untitled", onCompletion: { result in switch result { case let .success(url): logput("export success", url.absoluteString) case let .failure(error): logput(error.localizedDescription) } viewModel.isExporting = false }, onCancellation: { viewModel.isExporting = false } )
-
Data
を保存する場合、FileDocument
を使う
import SwiftUI
struct ContentView: View {
@State var selection = Set<Int>()
@State var items = [UUID]()
var body: some View {
VStack {
List(selection: $selection) {
ForEach(items.indices, id: \.self) { index in
Text(items[index].uuidString).tag(index)
}
}
Button {
items.append(UUID())
} label: {
Image(systemName: "plus")
}
Button {
items.remove(atOffsets: IndexSet(selection))
selection.removeAll()
} label: {
Image(systemName: "minus")
}
}
.onAppear {
items = (0 ..< 50).map { _ in UUID() }
}
}
}
MacBookの狂った時間を直す
$ sudo rm /var/db/timed/com.apple.timed.plist
$ sudo sntp -S ntp.nict.jp
$ ps -ef | grep timed
266 138 1 0 3:42PM ?? 0:00.18 /usr/libexec/timed
$ sudo kill 138
$ sudo sntp -S ntp.nict.jp