やりたいこと
https://live.nicovideo.jp/watch/lvxxxxx を自作 macOS アプリで受けたい。
が事の発端でした。
Info.plist の設定
まず、URL(https://)を受け取れるように、Info.plistのURL Typesを追加します。
https を URL Type に追加しても、既定ブラウザを奪うという意味ではなく、「このアプリも https URL を Viewer として扱える」と Launch Services に登録する、くらいの理解でよさそうです。
- CFBundleURLNameには、自分のアプリケーションのBundle Identifier + 処理する内容の識別子を書きます
- CFBundleURLSchemes は、https:// を受けとりたいのでhttpsと書きます
- Role はURLを編集するのではないのでViewerにします
作られたInfo.plistの中身を見るとこんな感じです
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>tv.from.chajka.Charleston.url</string>
<key>CFBundleURLSchemes</key>
<array>
<string>https</string>
</array>
</dict>
</array>
application(open:)の実装
NSApplicationDelegateを見ると、
func application(_ application: NSApplication, open urls: [URL])
func application(_ application: NSApplication, openFile: String) -> Bool
func application(_ application: NSApplication, openFiles: [String])
の3つがあって、func application(_ application: NSApplication, open urls: [URL]) を実装すると、func application(NSApplication, openFile: String) -> Boolやfunc application(NSApplication, openFiles: [String])は呼ばれないよと書いてあるので、func application(_ application: NSApplication, open urls: [URL])を実装します。
application(open:)が呼ばれない
ここまでできたら、Playgroundから、アプリケーションにURLを渡すSwiftコード素片を書いて実験です。
しかし、何度実行しても、application(open:)に置いたブレークポイントでプログラムが止まることはありませんでした。
ここでハマったらCodex君の出番です
NSAppleEventManager で kAEGetURL を拾う。
Codex君に相談すると、あっさり、
NSAppleEventManager.shared().setEventHandler
というものを教えてくれました。どうやら、URL OpenはApple Eventで来るようです。
そこで、privateな
@objc private func handleGetURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) {
guard let urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
let url = URL(string: urlString)
else {
print("Received URL event, but could not parse URL")
return
}
_ = application(openURLs: [url])
}
このイベントハンドラーでは、Apple Event から URL 文字列を取り出して、自前の共通処理 application(openURLs:) に渡します。
その中継メソッドとしてhandleGetURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor)を用意し、それを applicationWillFinishLaunchingで登録します。 具体的にはこんな感じ。
func applicationWillFinishLaunching(_ notification: Notification) {
NSAppleEventManager.shared().setEventHandler(
self,
andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)),
forEventClass: AEEventClass(kInternetEventClass),
andEventID: AEEventID(kAEGetURL)
)
}
つまりAppleEventManagerからkAEGetURLで、URL Open Apple Eventが来たらこの処理を呼び出してね。というのを登録します。
処理の中では共通で使いたいので、薄くapplication(openURLs:)を呼び出すだけにします。
ApplicationがURLを受け取る事を実験するPlayground
実際に、これで自作アプリケーションがURLを受け取るか?の実験(ドライバー)にはSwift Playgroundを使いました。
こんな感じ
import AppKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let liveURL = URL(string: "https://live.nicovideo.jp/watch/lvxxxxxxx")!
let charlestonAppURL = URL(fileURLWithPath: "/path/to/yourapp.app")
// または DerivedData 内の Charleston.app を直接指定
let configuration = NSWorkspace.OpenConfiguration()
configuration.activates = true
configuration.allowsRunningApplicationSubstitution = false
NSWorkspace.shared.open(
[liveURL],
withApplicationAt: charlestonAppURL,
configuration: configuration
) { app, error in
if let error {
print("Failed:", error)
} else {
print("Opened with:", app?.bundleIdentifier ?? "unknown")
}
PlaygroundPage.current.finishExecution()
}
僕はVeljaを使っているので、URLハンドリングには
configuration.allowsRunningApplicationSubstitution = false
で、特に気を使いました
感想
URLスキームにアプリケーションを対応させるには、URL Types を追加すれば終わりだと思っていました。
しかし実際には、Apple Event の世界まで降りることになりました。
古い仕組みではありますが、macOS のアプリ間連携の基盤として今もちゃんと生きています。これはドキュメントを眺めているだけでは気づきにくい、AppKitの地層の深くに眠っている資源を掘り起こした気分でした。
これを見て、「自分も https:// 」や、カスタムスキームに対応したいのだけどハマっていたという人の一助になれば幸いです。