Open58

1日1Zenn

KyomeKyome

NSFontで太字を使いたい

var font = NSFont(name: "フォント名", size: 18.0)!
font = NSFontManager.shared.convertWeight(true, of: font)

これでBoldになるっぽい

KyomeKyome

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
KyomeKyome

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")
    }
}
KyomeKyome

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のパーミッションについては取得できなかった。

KyomeKyome

Bluetooth構成(Bluetooth Configuration)を使ってBluetooth MIDI キーボードをつなぎ、QuickMIDIの対象として接続を開始した後にデバイス側からBluetooth接続をぶつ切りすると、アプリが死ぬパターンと死なないパターンがある。
Bluetooth構成の画面を開きっぱなしだと死なないのに、Bluetooth構成を閉じると死ぬ。
⇨CABTLEMIDIWindowController()を破棄せずに持つようにしたら死ななくなった...
メモリ的にちょっとやだ。

KyomeKyome

おー、CABTLEMIDIWindowController()をずっと保持するようにして使ってみたら、Bluetooth構成が明らかにおかしい感じなことがわかったぞ!MIDIキーボードの方はもう接続を切っているのに、Bluetooth構成では接続解除ボタンがまだ押せる状態で表示されてしまう。なんだこれ。

KyomeKyome

NSApp.orderFrontCharacterPalette(nil)で「絵文字と記号」のパレットを開くときのコツ

NSTextViewが設置してあるウィンドウでNSButtonなどをトリガーにして開くのは無理。2回開いた後に3回目が開けなくなる(謎)。何度でも機能するためにはNSMenu経由で開いた方がいい。

KyomeKyome

Xcodeプロジェクトをgit管理する時の.gitignore必須項目

# macだと勝手に発生するいらないファイル
.DS_Store
# Xcodeでプロジェクトを開いていた状態の記録(ソースを変更していなくても状態が変わるだけで差分が出てしまう)
*.xcuserstate  #

.gitignoreを更新した時にすでに差分管理されてしまっているファイルを除外する方法
https://qiita.com/kamesennin/items/226e3839e457b342b89f

$ git rm -r --cached .

をしてからadd commit pushすればいいだけ

KyomeKyome

macOS アプリのアプリ表示名を変更する方法

新規プロジェクトを作る時にSampleProjectみたいな名前にすると、そのままSampleProject.appという名前のアプリになると思う。そこをSample Project.appのようにスペースを入れたい場合など、表示名を変更したい場合の対処。

⭐️ Targetsでアプリのターゲットを選んで、Build SettingsでProduct Nameを探してその値を変更する。

  • PROJECTの方のProduct Nameを変更しても効果はない。
  • iOSでは効果があるInfo.plistのBundle display nameを変更しても効果はない。

参考: https://developer.apple.com/forums/thread/45718

KyomeKyome

MIDIUniqueID はmac端末ごとには別のIDが割り振られるが、
アプリを再起動しても、クリーンビルドしても同じIDが得られる。
macを再起動しても、同じIDが得られる。
USBのポートを切り替えても同じIDが得られる。
Bluetoothと有線の両方の機能を持っているMIDIキーボードの場合は、それぞれ別のIDを持っているが、切り替えてもIDそのものは変わらない。

同じ端末で、別のIDが割り振られるパターンがあるのか気になる。
=>とりあえず、不具合報告がくるまでは信用して良さそう。

KyomeKyome

node の path でホームディレクトリを取得する

const path = require("path");
path.resolve(process.env.HOME);
KyomeKyome

NSColorWell で 透明度のスライダーを表示させる方法

override func awakeFromNib() {
    super.awakeFromNib()
    NSColor.ignoresAlpha = false
}
KyomeKyome

NSButtonのクリック挙動

デフォルトだと、mouseDownはキャッチしているが、mouseUpはキャッチしていない。
むしろrightMouseDownとrightMouseUpはキャッチしている。ただし、右クリックの時はボタンのハイライトは変更されないし、クリックしてもコーディングしなければボタンを押したときのイベントは流れてこない。

左クリックしている間は他のUIの更新も止まる!

NSStatusBarButtonのクリック挙動

  • デフォルトではmouseDownとrightMouseUpがコールされる
  • mouseDownをoverrideして中で何も処理させないようにすると、mouseUpをコールするようになる。一方で.sendAction(on: [.leftMouseDown, .leftMouseUp])していてもactionはコールされなくなる
KyomeKyome

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も対応できる。

NSTextField+Extension.swift
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の許可設定をする必要がある。

参考

KyomeKyome

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]) {
}
KyomeKyome

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()
KyomeKyome

NSImageからPixel/inchを取得する方法

extension NSImage {
    var ppi: CGFloat {
        let rep = self.representations[0]
        return 72.0 * CGFloat(rep.pixelsWide) / self.size.width
    }
}
KyomeKyome

enum のケース名を出力する

enum Gomi {
    case hoge
    case meu
}

let hoge = Gomi.hoge
print(String(describing: hoge))
print(String(reflecting: hoge))
KyomeKyome

GitHub Actions のワークフローはトリガーがworkflow_dispatchの場合はデフォルトブランチでないと認識されない。

KyomeKyome

AWSのEC2では、/usr/share/vim/vimrc/ のなかに.vimrcをおけばどこでも.vimrcが効くようになる。

KyomeKyome

Homebrewでmariadbをインストールした場合のコマンドの叩き方

before after
mysql.server start brew services start mariadb
mysql.server stop brew services stop mariadb
brew services list
KyomeKyome

SwiftUI で MVVM

要素
View struct, View
ViewModel class, ObservableObject
Model protocol & struct

だいたいの流れ

MyApp.swift
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: ViewModel(model: MyModel()))
        }
    }
}
Model
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
    }
}
ViewModel
@MainActor
class ViewModel: ObservableObject {
    @Published var model: Model
    
    var value1: Int {
        return model.value1
    }

    func method() {
        model.method()
    }
}
View
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()
        }
    }
}
KyomeKyome

オブジェクトを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)
}
KyomeKyome

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)
$ 
KyomeKyome

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)
    }
}
KyomeKyome

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 に生成される。

KyomeKyome

SwiftUI Ventura でListを使うときにキー入力での意図しない行選択を無効にする

SwiftUIのListの内部実装はNSOutlineViewでできている。そのallowsTypeSelectプロパティをfalseにしてやれば、表題の意図しない操作を無効にできる。

extension NSOutlineView {
    open override var allowsTypeSelect: Bool {
        get {  return false }
        set {}
    }
}
KyomeKyome

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()だと条件分岐しずらいので)

KyomeKyome

常駐型アプリでキーボードしか使えなくなった時の対処法

  1. command + Tab でXcodeを選択
  2. command + . でXcodeの実行を止める
  3. SpotlightでActivity Monitor.appを開く
  4. option + command + F で検索 -> 名前を絞る
  5. command + A で全選択
  6. option + command + Q で終了する
KyomeKyome

メニューバーの高さを取得する方法

if let screen = NSScreen.main {
    let height = screen.frame.height - screen.visibleFrame.height
}
KyomeKyome

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()
}
KyomeKyome

SwiftPMのキャッシュを消す

  • ~/Library/Caches/org.swift.swiftpmにある該当パッケージの情報
  • ~/Library/org.swift.swiftpmにある該当パッケージの情報
  • ~/Library/org.swift.swiftpm.lockにある該当パッケージの情報
  • DerrivedDataSourcePackages
  • ローカルパッケージ内の.swiftpm
KyomeKyome

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) }
KyomeKyome

fileExporterの使い方

  1. 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
        }
    )
    
  2. Dataを保存する場合、FileDocumentを使う
KyomeKyome
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() }
        }
    }
}
KyomeKyome

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