Finderからファイルをドラッグ&ドロップする

macOS Swift

Finderからファイルをドラッグ&ドロップする

macOS Mojava 10.15.7 / Xcode 11.3.1 / Swift 5.0
ドラッグ&ドロップは、ペーストボードを介して、異なるアプリケーション間でオブジェクトを受け渡す技法である。実装方法の説明にあたって、実用的なアプリケーションを例にしたいと思う。
ドラッグ&ドロップされたオブジェクトを操作するためには、受け入れ側のオブジェクトが NSDraggingDestinationプロトコルに適応していなければならない。NSViewクラスおよびそのサブクラスはデフォルトでに宣言済みである。このプロトコルに定義されたメソッドを利用(オーバーライド)し、各種機能を実現していく。

Finderから画像ファイルをイメージビューにドラッグ&ドロップし表示する

実装は非常に簡単である。NSImageViewのサブクラスを作成し、draggingEnteredメソッドをオーバーライドするだけでよい。ドロップを契機にイメージをビューの表示(draw)するといった本来の処理は、NSImageViewクラスに実装済みなので、アプリケーションは何もしなくてよい。
[image5.png]

応用:ドラッグ&ドロップされたファイルのパス名を取得する

ドラッグ&ドロップされたファイルのパス名を取得する。performDragOperationメソッドをオーバーライドする。これはユーザがマウスのドロップ操作を行なったときに起動する。
Finderからファイルをドラッグ&ドロップした場合、受け渡されるのはファイルのパス名であり、実体は NSURLオブジェクトである。performDragOperationメソッドの info引数からペーストボードを参照し、その中からNSURLオブジェクトを取得すればパス名を得ることができる。
[image6.png]

カスタムビューに表示してみる

NSImageViewオブジェクトにではなく、NSViewクラスのオブジェクトにイメージを表示させてみる。
イメージの表示処理は、アプリケーションが行う必要があるので、NSViewクラスのサブクラスに実装する。NSURLオブジェクトに指定されたイメージファイルを読み込み、オブジェクトに保持する。drawメソッドをオーバーライドし、needDisplay を契機として、イメージをビューに描画する。なお、イメージのサイズはビューのサイズに合わせる、例題の画像では縦長になってしまう。サイズの調整もアプリケーションで対応する必要がある。
[image7.png]

Finderの複数ファイルをテーブルビューにドラッグ&ドロップする

Finderから選択した複数のファイルをテーブルビューの上にドラッグ&ドロップするとパス名の一覧が表示される。
[image8.png]
テーブルビューは NSTableViewクラスを中心にして複数のクラスから構成される。ドラッグ&ドロップイベントを補足するのは、その中の NSScrollViewクラスである。
NSDraggingDestinationプロトコルのメソッドはこのクラスのサブクラスの中に実装する。基本的には、上述した NSViewクラスにイメージを表示する処理と同じである。
(1) 受け入れオブジェクトタイプの指定
ドラッグ&ドロップで受け入れ可能なオブジェクトのタイプを指定する。ここで受け渡すオブジェクトは URLクラスのファイルURLオブジェクトである。これ以外のオブジェクトはドラッグに対して何も反応しない。
タイプの一覧はNSPasteboard.PasteboardTypeに定義される。ここにあるタイプのオブジェクトがペーストボードを介した受け渡しが可能である。URLの他に、テキスト、pdf、イメージなどが指定できる。
[ 参考 ]
PasteboardTypeは、UTI(uniform type identifier)の一部に相当する。UTIは Cocoaフレームワークで利用されるオブジェクトを定義した一覧表である。オブジェクトの種類を特定の文字列で表現したもので、形式はURLアドレスのドメインに似ている。「public.item」を頂点とした系統樹のような構造になっていて、階層が細分化するに従い具体的なオブジェクトタイプになる。
CocoaフレームワークにおけるUTI一覧 に全種類を示す。これを利用すれば、あらゆる種類のオブジェクトを個別に、または集合的に指定することができる。
(2) ドラッグオブジェクトがビューに入ったときの処理( draggingEnteredメソッド )
ドラッグしたオブジェクトがビューの中に入ったときに一度起動する。オブジェクトを受け入れるアプリケーションは必ず実装して、リターンコードに NSDragOperation定数のうち何かを返さなくてはならない。 一般的にが、generic を返してしておけばよい。
メソッドの戻り値について
オブジェクトのドロップ処理においては、ここで何を返そうと、結果には何の影響も与えないことに留意すること。
ドラッグ&ドロップでは、オブジェクトを受け入れたアプリケーションがドロップ処理の結果をオブジェクトの送り元のアプリケーションに通知する仕組みが用意されている。本例では送り元のアプリケーションは Finderということになる。
ドロップ処理(後述する performDragOperationメソッド)は、処理の結果として Bool値(成功/失敗)を返す。それに対し、オブジェクトの送り元のアプリケーションは、もしドロップの完了通知を受け取るデリゲートメソッドを実装していれば、この結果を受け取ることができる。そのとき受け取る情報は 成功/失敗の Bool値でなく、ここで定義した draggingEnteredメソッドの戻り値である NSDragOperation定数(本例では generic)となる。
成功した場合はその値を受け取り、失敗した場合は何も受け取らない(実際は rawValue(0) が返る)。オブジェクトの送り元のアプリケーションは、例えば、戻ってきた値が が moveなら送り出したオブジェクトを削除する、copyなら削除しないという操作を行うことができる。
この仕組みをどのように使用するかは、オブジェクトの送り側と受け入れ側の間で自由に決めてくださいということである。
(3) ドロップの準備( prepareForDragOperationメソッド )
ドロップを実行する直前に起動するので何かしらの前処理があれば行う。ドロップを受け入れることができない状態であれば、戻り値に falseを返し、処理を終わらせることができる。このメソッドは必要なければ実装しなくともよい。
(4) ドロップの実行( performDragOperationメソッド )
ドロップしたときの処理を記述する。引数の NSDraggingInfoオブジェクトからペーストボードに格納されているオブジェクトをオブジェクトの種類(forClasses)を指定して読み込むことができる。
本例では、NSURLオブジェクトを読み込む。オブジェクトは複数ある場合がある。そこからファイルのパス名を取得し、テーブルビューのデータソースに書き込み、reloadDataメソッドでテーブルビューに表示する。処理が成功した場合は true、失敗した場合は falseを返す。
なお、メソッドが扱える URLは、NSURLクラスのオブジェクトとして読み込む必要があるので注意すること。
ソースコード

ドラッグ&ドロップの送り側となる

ユーザー作成アプリケーションがドラッグ&ドロップの送り側になる方法について説明する。
前述のイメージビューに画像を表示するアプリケーションをベースに改造し、送り出すオブジェクトは引き続きファイルURLとする。具体的にはイメージビューに表示している画像をドラッグし、Googleレンズにドロップするようなことができる。
[image9.png]
(1) ドラッグ&ドロップされたファイルのパス名を取得する
他アプリケーション(Finderなど)からイメージファイルをドラッグ&ドロップしてイメージビューに表示する。このときイメージファイルの URLオブジェクトをオンスタンス変数に保持する。前述「ドラッグ&ドロップされたファイルのパス名を取得する」とほぼ同じ処理。
(2) ドラッグ&ドロップ操作の開始
mouseDownイベントハンドラを実装する。送り出すオブジェクトはイメージファイルの NSURLオブジェクトである(URLオブジェクトではない)。これを NSDraggingItemオブジェクトに変換し配列に追加する。配列に複数のオブジェクトを格納すれば1回のドラッグ&ドロップで複数のオブジェクトを送り出すことができる。この配列を引数にして beginDraggingSessionメソッドを開始すればドラッグ&ドロップの送り出しが始まる。
NSDraggingItemクラスの setDraggingFrameはマウスでドラッグの途中、ポインターに追随するい小さな画像を指定するためのメソッドである。画像のサイズと画像オブジェクトを指定することができる。本例では、イメージビューの画像を100x100ピクセルの大きさで表示している。(なので若干縦に歪む)
(3) ドラッグ&ドロップ操作の宣言
draggingSessionメソッドを実装して実行可能なドラッグ&ドロップ操作を宣言する。メソッドの戻り値に操作のタイプを指定する。ここでは、"copy" を指定する。常に汎用的な "generic" でよいと考えていたが、Googleレンズではなぜか弾かれてしまう(どうも最近変わったようだ)。これは受け入れ側のアプリケーションとの取り決めで決まるものだと思うが、よくわからない、
(4) 完了処理・必要な場合だけ実行する
送り出し先のアプリケーション(Googleレンズなど)のドロップ操作が完了したときに起動する。例えばドラッグ&ドロップの操作がオブジェクトの "move" であれば、オブジェクトの削除はアプリケーション側がこの時点で行う。本例では、送り出し先のアプリケーションからの戻り値の表示のみを行う。
全ソースコード