🐰

macOSのアプリアイコンにテキスト等を直接ドロップして受け取れるようにする

2024/05/13に公開

macOSアプリには、アプリアイコンに直接データをドロップして受け取ることができる仕組みがあります。例えばテキストを長押しでドラッグ開始して、Finder上のアプリアイコンまたはDock上のアプリアイコンにドロップすることでそのテキストを直接受け取って処理するなどのインタラクションが実現可能です。ファイルに保存してから開き直したりするよりも、ドラッグ&ドロップによって直接データを読み込むことができるので、直感的なインタラクションを実現可能です。

これの実現にはmacOSの古い仕組みである「サービス」とペーストボードを使います。

方針

  • Info.plistにNSServicesの定義を追加する
  • サービスプロバイダー(サービスを提供するクラス)を用意する
  • サービスプロバイダーにデータを受け取るメソッドを実装する → NSMessageでメソッド名を定義
  • NSAppにサービスプロバイダーのインスタンスをセット

NSServicesの設定

まずInfo.plistに次のような定義を追加します。NSServicesをキーとする配列を作り、辞書形式で必要な属性を記述します。

  • NSServices (array<dictionary>)
    • 定義のルート。複数の定義を持ちたい場合には、この配列以下に辞書を都度定義するのが良いと思われる
  • NSMessage (string)
    • サービスをトリガーにして呼び出すサービスプロバイダー(サービスを提供するクラス)で呼び出す任意のメソッド名(名前は自由)
  • NSSendTypes (array<string>)
    • サービスが読み取れるデータタイプを宣言
    • テキストを受け取るにはNSStringPboardTypeを一つ記述
  • NSPortName (string)
    • ポート名。ほとんどの場合これはサービスプロバイダーのアプリケーション名
  • NSMenuItem (dictionary<string : string>)
    • defaultをキーとしてメニュー名を宣言
    • サービスメニュー用に使われる

詳しくはドキュメントを参照。

Info.plist(一部)
<key>NSServices</key>
<array>
	<dict>
		<key>NSMessage</key>
		<string>retrieveData</string>
		<key>NSSendTypes</key>
		<array>
			<string>NSStringPboardType</string>
		</array>
		<key>NSPortName</key>
		<string>$(PRODUCT_NAME)</string>
		<key>NSMenuItem</key>
		<dict>
			<key>default</key>
			<string>New Text with $(PRODUCT_NAME)</string>
		</dict>
	</dict>
</array>

コード

サービスを提供するクラス(サービスプロバイダー)を用意し、NSMessageに設定した名前と同じインスタンスメソッドを実装します。サービスが実行されるとこのメソッドが呼び出されるので、必要な処理を実装できます。このメソッドは決まった引数を取るので注意してください。Swiftからの場合は@objc修飾子をつけることも忘れずに。

今回の例ではNSMessageに “retrieveData” を記述したので、その場合のメソッドは以下のような形式になります。NSMessageの定義が異なる場合には、以下の引数はそのままに、メソッド名のみを任意の名前に差し替えてください。

@objc func retrieveData(_ pboard: NSPasteboard,
                        userData: String,
                        error errorPointer: AutoreleasingUnsafeMutablePointer<NSString?>)

以下がサービスプロバイダーの全実装です。クラス名は任意です。今回はテスト用に音を鳴らして反応できるようにしてみます。

ServicesProvider.swift
import Cocoa

class ServicesProvider {
	
	@objc func retrieveData(_ pboard: NSPasteboard, userData: String, error errorPointer: AutoreleasingUnsafeMutablePointer<NSString?>) {
		// test1: react with sound
		NSSound(named: "Sosumi")?.play()
		
		// test2
		if let types = pboard.types {
			for type in types {
				print(#function, "\(type) \(String(describing: pboard.string(forType: type)))")
			}
		}
		
		// extract dropped string from the pasteboard
		if let receivedString = pboard.string(forType: .string) {
			print(#function, "\(receivedString)")
		}
	}

}

入力されたデータ(今回はテキストデータ)を取得するには、NSPasteboardから取り出してください。

そして最後の仕上げとして、NSApplicationインスタンスのservicesProviderプロパティにセットして完了です。applicationDidFinishLaunching()のタイミングで良いでしょう。

AppDelegate.swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
	NSApp.servicesProvider = ServicesProvider()
	NSUpdateDynamicServices()

}

参考資料

サンプルコード:
https://github.com/usagimaru/ServiceTest

Discussion