Open16

Finder Sync Extension

Finder Sync Extension

そもそも

  • ローカルファイルとリモートファイルを同期するアプリのために用意されたもの
    • そのようなアプリ(DropBoxやGoogle Drive)はFSEを含める必要がある
  • Finder上でバッジを表示して、アイテムの同期ステータスを示すことができる
    • バッジという形式でアイテムの状態について示すことが可能
  • パスワード保護の優先や追加など、ファイルおよびフォルダーの管理タスクを実行するようなカスタムコンテキストメニュー項目を提示できる
    • ファイルの同期だけにとどまらず、基本的にはどんな処理も実行可能
    • 選択したディレクトリやファイルに関するアクションが可能(権限もユーザが選択したものということで特別に付与する必要がない)
  • 同期操作の強制など、グローバルアクションを実行するカスタムツールバーボタンを提供できる
    • 単一ディレクトリやファイルに限らず、グローバルにアクションを実行可能

できること

  • 1つ以上のフォルダをシステムに監視させられる
  • 監視対象フォルダ内のアイテムにバッジ、ラベル、コンテキストメニューを設定できる
  • ツールバーボタンをFinderウィンドウに追加できる(これは監視対象フォルダを参照していないタイミングでも表示される)
  • 監視対象フォルダのサイドバーアイコンを追加できる
  • ユーザが監視対象フォルダの閲覧を開始または停止したときに通知を受け取れる

サイドバーのアイコンを変更する方法

  • コンテナアプリでIconにAsset Catalogを使うのをやめる
  • 代わりに任意の名前.iconsetというフォルダを参照するようにする
    • コンテナアプリ側に上記のフォルダをドラッグ&ドロップしてリソースを追加する
    • コンテナアプリのInfo.plistでCFBundleIconFileに.iconsetより前を指定する
    • TAEGETSのGeneralのApp Iconsの項目でSourceがAsset Catalogを指していた場合はDon't useを指定する

.iconsetフォルダに含める画像ファイル

  • icon_16x16.png
  • icon_16x16@2x.png
  • icon_32x32.png
  • icon_32x32@2x.png
  • icon_128x128.png
  • icon_128x128@2x.png
  • icon_256x256.png
  • icon_256x256@2x.png
  • icon_512x512.png
  • icon_512x512@2x.png
  • sidebar_16x16.png
  • sidebar_16x16@2x.png
  • sidebar_18x18.png
  • sidebar_18x18@2x.png
  • sidebar_32x32.png
  • sidebar_32x32@2x.png

サイドバーアイコンに関しては、透明か黒かの2色のみを用いたpngファイルを用いる必要がある。黒に透明度がかかっているとダメ。左の画像はダメだが右の画像はOK。

例

ファイルにバッジをつける方法

FinderSync の init でバッジ用画像の指定をする

FinderSync.swift
override init() {
    // 中略
    let image = NSImage(named: "Hiyoko")
    FIFinderSyncController.default()
        .setBadgeImage(badgeImage1, label: "Label" , forBadgeIdentifier: "🐤")
}

requestBadgeIdentifierでバッジを指定する

FinderSync.swift
override func requestBadgeIdentifier(for url: URL) {
    // たとえば、拡張子がpngの時はPNG用のバッジをつける
    if url.pathExtension == "png" {
        FIFinderSyncController.default().setBadgeIdentifier("PNG", for: url)
    }
}

この機能は、監視対象のフォルダが他のFinder Sync Extentionの監視対象に内包されていない時のみ発動できる。たとえば環境にDropBoxやGoogle Driveなどがインストールされており、その拡張が有効になっている場合は発動できない。より親のフォルダを監視対象にしている拡張に負けてしまう。

デバッグの方法

大前提として、Swift.print()によるprint debugはできない。processにアタッチしても標準出力はキャッチできない。

GUIでデバッグする方法

  • context menu を押したときの挙動チェック → NSAlert()を使う。
  • 監視対象をFinderで表示開始・停止したときの通知 → UNNotificationRequestを使う。

Xcodeでデバッグする方法

  • breakpointを使ったデバッグ
    • Debug -> Attach to process でプロセスにアタッチする。
    • process はFinderで監視対象のフォルダを開くたびに新しく作られる(拡張機能の新規インスタンスが作られる)ので、Attach to processが増えてきた時は、Terminalでkillall Finderをすればいい。

コンソールでデバッグする方法

  • NSLogとコンソール.appを使ってprint debugができる。
    • NSLog("✨: \(variables)")のようにして、コンソールアプリで✨に絞り込みをすれば動作確認ができる。
    • こちらもやる前にprocessが一つかどうか確認して、複数あるならkillall Finderする。

機能アイデア

  • 選択したフォルダ内にPreview.appの新規キャンバスを作成するツール
  • 選択したファイル群を連番でリネームするツール
  • 選択した画像を1倍 2倍 3倍の画像セットで出力する

ファイルのread/write権限について

StackOverflowの投稿にあるように、FIFinderSyncController.default().selectedItemURLs()はユーザが選択したファイルのURL群であるにもかかわらず、entitlementsのcom.apple.security.files.user-selected.read-writeには当たらずread/writeすることはできない。

read/writeする場合は、com.apple.security.temporary-exception.files.absolute-path.read-writecom.apple.security.temporary-exception.files.absolute-path.read-writeにパスを指定する必要がある。ただし、これだとAppStoreへの審査提出でリジェクトになる可能性がある。審査のメモ欄に一時的な権限の必要な理由を書いて交渉してみる価値があるかは微妙?

FinderSyncのインタフェースをNSAlertを用いて実装するときのコツ

  • AccessoryViewの実装の仕方
    • NSTextFieldでのcut, copy, paste, select allの受け付け方
    • NSTextFieldのフォーカスをtabで移動する方法

コンテキストメニューの追加

override func menu(for menuKind: FIMenuKind) -> NSMenu {
    // コンテキストメニューを開く方法ごとに処理を分ける時    
    switch menuKind {
    case .contextualMenuForContainer:
        // フォルダに対して右クリックをした時
    case .contextualMenuForItems:
        // ファイルに対して右クリックをした時
    case .contextualMenuForSidebar: // なぜか呼ばれない(macOS 11)
        // サイドバーのフォルダを右クリックした時のはず
    case .toolbarItemMenu:
        // ツールバーのボタンをクリックした時
    @unknown default:
        fatalError()
    }

    let menu = NSMenu(title: "")
    let item = NSMenuItem(title: "コマンドのタイトル",
                          action: #selector(someAction(_:)),
                          keyEquivalent: "")
    
    // メニューアイテムにアイコンをつけたい場合
    let id = "メインアプリのBundle Identifier"
    if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: id) {
        item.image = NSWorkspace.shared.icon(forFile: url.path)
    }
    menu.addItem(item)
    
    return menu
}
@IBAction func someAction(_ sender: AnyObject?) {
    // メニューを開いたフォルダのURL
    let target = FIFinderSyncController.default().targetedURL()
    // 選択していたファイル群のURL(ファイルを選択していたら取れる)
    let items = FIFinderSyncController.default().selectedItemURLs()
}

override func menu(for menu: FIMenuKind) -> NSMenu? { }のまま使うと、NSMenuなのかFIMenuKindなのかわかりにくいので、menumenuKindにするのがおすすめ。(参考

  • macOS 10.11未満だとNSMenuのsubmenuには対応してないらしい。
  • サイドバー右クリックでコンテキストメニューが出てこない。

ツールバーボタンの追加

以下の3つをoverrideして定義すると追加されるようになる。

// ツールバーのカスタマイズの欄で表示される
override var toolbarItemName: String {
    return "FinderSyncSample"
}

// ツールバーボタンの上でホバーした時に表示される
override var toolbarItemToolTip: String {
    return "Toolbar Button"
}

// ボタンのアイコン(1倍 18px、2倍 36px)
override var toolbarItemImage: NSImage {
    return NSImage(named: "Icon")!
}

拡張を有効にしても、Finder上で表示されない場合は、表示 -> ツールバーのカスタマイズでボタンを追加する。

Tips

  • Finder Sync Extensionのフレームワークは基本的にObjective-Cで書かれている

監視するファイルの指定の仕方

AppSand Box を ON にしていると、NSHomeDirectory()FileManager.default.homeDirectoryForCurrentUserの指すパスが/Users/UserName/ではなく/Users/UserName/Library/Containers/.../Dataになる。

NSImage(contentsOf: URL) で画像を読み込むことができない...
まじで権限きつい

Finder Sync Extension の表示名について

  • Big Sur だとシステム環境設定の中では収容アプリ側しか名前が表示されない。
  • Catalina だと Bundle display name が表示される。
ログインするとコメントできます