Open4

Xcode: macOSでドラッグ&ドロップ起動するようにする

kabeyakabeya

macOSアプリです。
ファイルをアプリのアイコンにドラッグ&ドロップして起動させるタイプのアプリを作ろうと思ったんです。
Xcodeに使うドキュメントタイプを設定する(というかInfo.plistを作る)、というのは分かったのですが、ここが結構XcodeのバージョンなのかmacOSのバージョンなのか、ともかく参考にするページごとに違っていて、混乱するんですね。

現行のXcode 15.4でちょっと書いていきます。

TARGETS→Info→Document Typesに、開くドキュメントの型を登録していきます。

Xcode 15.4時点では、以下のような項目があります。

  • Name:適当に入力
  • Identifier:ここがキモです。UTI(Uniform Type Identifier)を入れる必要があります。独自のファイル形式の場合は、逆DNS記法で他のアプリとかぶらないようなものを指定します。
  • Class:空でOK。使う場合は自分のドキュメントクラスを指定します。
  • Role:読み込みしかしなければViewer、保存もする場合はEditorを指定します。
  • Legacy Icon:空でOK。macOS 10.15以前のOSでは古き.icnsとかを使ったアイコンを表示するらしいんですが、それ用ですね。
  • Handler Rank: このIdentiferを定義したのが自分か(Owner)、そうでないか(Alternative)、ということですね。意味合いとしては、ドキュメントアイコンをダブルクリックしたときにデフォルトで起動するアプリかどうか、ということになると思います。

publicでないIdentiferを利用する場合は、Exported Type Identifiers(自分のアプリで定義する場合)、Imported Type Identifiers(3rdパーティ製ファイル定義を利用する場合)も定義する必要があります。
詳しくは以下を見るといいですね。

https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app

独自アイコン作成方法は以下を見るといいですね。

https://developer.apple.com/news/?id=5i6jlf4d

kabeyakabeya

UTTypeに定義されている型ならPlaygroundで以下のようにしてIdentiferを確認できます。

import UniformTypeIdentifiers

let t = UTType.midi   // 例えばMIDIファイルの場合
print("\(t.identifier)")
kabeyakabeya

ちなみに。
Xcode 14以降、Document Types、Exported Type Identifers、Imported Type Identifiersは、削除する機能がなくなっているようです。バグなのか仕様なのか。バグじゃないかと言われていますが。

なので間違って追加したら、Info.plistを開いて直接編集して削除する必要があります。
TARGETS→InfoはInfo.plistのエディタではあるんですが、ファイルを直接編集したことを検知しません。
Info.plistを直接編集すると、その後にTARGETS→Infoで編集した際に不整合が生じます。

Info.plistを直接編集したら、いったんプロジェクトファイルを閉じて開き直すのがいいと思います。
これもバグに近い仕様というか。これ直してくれると世界中の開発者の生産性があがると思うんですけどね。

kabeyakabeya

アプリ側の対応ですが、SwiftUIの場合は以下のようになります。
まず、アプリ側にAppDelegateを設定してやる必要があります。

@main
struct MyGreatApp: App {
    @NSApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class MyAppDelegate: NSObject, NSApplicationDelegate {
    func application(sender: NSApplication, openFile filename: String) -> Bool {
        print("\(filename)")
        return false
    }
    
    func application(_ application: NSApplication, open urls: [URL]) {
        print("\(urls)")
    }
}

最初はopenFileのほうだけを実装して試したんですが、来なかったんです。
もう一つのopenのほうを実装したら来ましたね。

さらに、以下の説明では、openを実装するとopenFileopenFilesは来なくなる、と書いてあります。
もうopenだけで良さそうです。

https://developer.apple.com/documentation/appkit/nsapplicationdelegate/2887193-application