[SwiftUI] SwiftUI で実装する Drag&Drop (その2:クラスオブジェクト を Drag&Drop)

SwiftUI

Drag&Drop 実装について まとめていきます。今回は、自分で定義したクラスオブジェクトを ドラッグ&ドロップに対応させる方法です。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.2.3
  • Xcode 12.4
SwiftUI[SwiftUI] SwiftUI で実装する Drag&Drop (その1: String を Drag&Drop)

自分で定義したクラスの対応

前回は、onDrag, onDrop で定義した closure の中で、NSItemProvider 経由で渡すデータの作成を行なって、ドラッグ&ドロップを実現していました。

自分の定義したクラスを使う時には、これらの処理を自クラスの中に入れ込むことができます。

クラスオブジェクトを受け渡しするためのプロトコル NSItemProviderReading, NSItemProviderWriting

独自に定義したクラスも、前回説明した方法で Data に変換することで、受け渡しすることができますが、もう1つ別の方法もあります。

そのための Protocol として、NSItemProviderReading と NSItemProviderWriting があります。

NSItemProviderReading は、ドロップされたときに オブジェクトを構築するために使用されます。(ドロップ時に、NSItemProvider から オブジェクトを受け取れるようにします)

NSItemProviderWriting は、ドラッグするときに、オブジェクトを提供するために使用されます。(ドラッグ時に、NSItemProvider に オブジェクトを提供できるようにします)

# いずれも、実際のデータ受け渡しは、ドラッグ&ドロップ操作が実行された時になります。

ベースとするクラス

example

// (1)
final class MyDropData: NSObject {
    // (2)
    static let dragDropID = "public.utf8-plain-text"
    // (3)
    var str: String
    init(_ str:String) {
        self.str = str
    }
}
コード解説
  1. NSItemProviderReading, NSItemProviderWriting に準拠するためには、class であることが必要で、NSObject を継承していないといけません。
  2. UTType として、"public.utf8-plain-text" を利用しています。
    独自定義のものを使うには追加手順が必要になるので省略のためです
  3. String を保持するだけのクラスです。

このクラスに、NSItemProviderReading, NSItemProviderWriting を実装していきます。

エラー処理のために以下のような enum を定義していますが、強い関係はありません。

example

enum MyError: Error {
    case unknownType
    case generateError
}

NSItemProviderReading

定義しなければいけないものは2つです。「受け取ることができるタイプ」と「受け取ってと言われた時のデータ受け取り方法」です。
それぞれ、static var readableTypeIdentifiersForItemProvider と static func object(withItemProviderData data: Data, typeIdentifier: String) を定義することで設定します。

Apple のドキュメントは、こちら

NSItemProviderReading

    // (1)
    static var readableTypeIdentifiersForItemProvider: [String] { [dragDropID] }
    // (2)
    static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> MyDropData {
        // (3)
        guard typeIdentifier == dragDropID else { throw MyError.unknownType }
        // (4)
        if let str = String(data: data, encoding: .utf8) {
            return MyDropData(str)
        }
        throw MyError.generateError
    }
コード解説
  1. readableTypeIdentifiersForItemProvider として、 "public.utf8-plain-text" を指定しています
  2. この関数が、ドロップされたときに、onDrop 内で ドラッグ&ドロップのデータからクラスを構築するために使用されるメソッドです。(static です)
  3. タイプをチェックして、想定していないタイプであればエラーとします
  4. 渡されるデータから String を復元し、MyDropData クラスのインスタンスを作成して返しています
オブジェクトの構築は、上記のように static メソッドの中で行われるため、必要な情報は全て、ドラッグ&ドロップ時のデータに含めておく必要があります。

NSItemProviderWriting

Reading と同様に定義しなければいけないものは2つです。「渡すことができるタイプ」と「渡してと言われた時にデータを渡す方法」です。
それぞれ、static var writableTypeIdentifiersForItemProvider と func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? を定義することで定義します。

Apple のドキュメントは、こちら

NSItemProviderWriting

    func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
        // (1)
        let data = str.data(using: .utf8)
        // (2)
        completionHandler(data, nil)
        // (3)
        return nil
    }
コード解説
  1. String を受け渡すために、String を Data 化しています
  2. completionHandler 経由で、Data を受け手に渡します
  3. Progress を返すことで途中でのキャンセル等にも対応させることが可能です

NSItemProviderReading/NSItemProviderWriting の有無で比較

機能的に差異はありませんが、コードを並べておきます。

NSItemProviderReading

NSItemProviderReading

Text(dragText1)
    .onDrag {
        let provider = NSItemProvider()
        provider.registerDataRepresentation(forTypeIdentifier: kUTTypeUTF8PlainText as String, visibility: .all) { (completion) -> Progress? in
            completion(dragText1.data(using: .utf8), nil)
            return nil
        }
        return provider
    }
Text(dragText2)
    .onDrag  {
        return NSItemProvider(object: MyDropData(dragText2))
    }

NSItemProviderWriting

NSItemProviderWriting

Text(dropText1)
    .onDrop(of: [kUTTypeUTF8PlainText as String], isTargeted: $isTarget1) { providers -> Bool in
        guard let provider = providers.first else { return false }
        guard provider.hasItemConformingToTypeIdentifier(kUTTypeUTF8PlainText as String) else { return false }
        provider.loadItem(forTypeIdentifier: kUTTypeUTF8PlainText as String, options: nil) { (data, error) in
            if let str = String(data: data as! Data, encoding: .utf8) {
                dropText1 += " \(str)"
            }
        }
        return true
    }
Text(dropText2)
    .onDrop(of: MyDropData.readableTypeIdentifiersForItemProvider, isTargeted: $isTarget2) { providers -> Bool in
        guard let provider = providers.first else { return false }
        guard provider.canLoadObject(ofClass: MyDropData.self) else { return false }
        provider.loadObject(ofClass: MyDropData.self) { (data, error) in
            if let myDropData = data as? MyDropData {
                dropText2 += " \(myDropData.str)"
            }
        }
        return true
    }

テストコード全体

前回のコードに今回のコードを追加した最終形は以下の通りです。

MyDropData,ContentView, MyError

enum MyError: Error {
    case unknownType
    case generateError
}

final class MyDropData: NSObject, NSItemProviderReading, NSItemProviderWriting {
    static let dragDropID = "public.utf8-plain-text"

    var str: String
    init(_ str:String) {
        self.str = str
    }
    
    static var readableTypeIdentifiersForItemProvider: [String] { [dragDropID] }
    static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> MyDropData {
        guard typeIdentifier == dragDropID else { throw MyError.unknownType }
        if let str = String(data: data, encoding: .utf8) {
            return MyDropData(str)
        }
        throw MyError.generateError
    }

    static var writableTypeIdentifiersForItemProvider: [String] { [dragDropID as String] }
    func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
        let data = str.data(using: .utf8)
        completionHandler(data, nil)
        return nil
    }
}
struct ContentView: View {
    @State private var dragText1 = "Drag from"
    @State private var dragText2 = "Drag from"
    @State private var dropText1 = "Drop to"
    @State private var dropText2 = "Drop to"

    @State private var isTarget1 = false
    @State private var isTarget2 = false
    var body: some View {
        HStack {
            List {
                Text(dragText1)
                    .onDrag {
                        let provider = NSItemProvider()
                        provider.registerDataRepresentation(forTypeIdentifier: kUTTypeUTF8PlainText as String, visibility: .all) { (completion) -> Progress? in
                            completion(dragText1.data(using: .utf8), nil)
                            return nil
                        }
                        return provider
                    }
                Text(dragText2)
                    .onDrag  {
                        return NSItemProvider(object: MyDropData(dragText2))
                    }
            }
            List {
                Text(dropText1)
                    .onDrop(of: [kUTTypeUTF8PlainText as String], isTargeted: $isTarget1) { providers -> Bool in
                        guard let provider = providers.first else { return false }
                        guard provider.hasItemConformingToTypeIdentifier(kUTTypeUTF8PlainText as String) else { return false }
                        provider.loadItem(forTypeIdentifier: kUTTypeUTF8PlainText as String, options: nil) { (data, error) in
                            if let str = String(data: data as! Data, encoding: .utf8) {
                                dropText1 += " \(str)"
                            }
                        }
                        return true

                    }
                Text(dropText2)
                    .onDrop(of: MyDropData.readableTypeIdentifiersForItemProvider, isTargeted: $isTarget2) { providers -> Bool in
                        guard let provider = providers.first else { return false }
                        guard provider.canLoadObject(ofClass: MyDropData.self) else { return false }
                        provider.loadObject(ofClass: MyDropData.self) { (data, error) in
                            if let myDropData = data as? MyDropData {
                                dropText2 += " \(myDropData.str)"
                            }
                        }
                        return true
                    }
            }
        }
    }
}
注意
UTType として、public.utf8-plain-text を間借りしているので、気をつけてください。

まとめ:自分で定義したクラスのドラッグ&ドロップ対応

自分で定義したクラスのドラッグ&ドロップ対応
  • NSItemProviderReading に準拠することで、Drag 時に、オブジェクトを渡すことで、NSItemProvider が 対応タイプ含め自動で処理してくれる
  • NSItemProviderReading に準拠することで、Drop 時に、NSItemProvider から オブジェクトとして受け取ることが可能となる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です