Sponsor Link
環境&対象
- macOS Big Sur 11.2.3
- Xcode 12.4
[SwiftUI] SwiftUI で実装する Drag&Drop (その1: String を Drag&Drop)
自分で定義したクラスの対応
前回は、onDrag, onDrop で定義した closure の中で、NSItemProvider 経由で渡すデータの作成を行なって、ドラッグ&ドロップを実現していました。
自分の定義したクラスを使う時には、これらの処理を自クラスの中に入れ込むことができます。
クラスオブジェクトを受け渡しするためのプロトコル NSItemProviderReading, NSItemProviderWriting
独自に定義したクラスも、前回説明した方法で Data に変換することで、受け渡しすることができますが、もう1つ別の方法もあります。
そのための Protocol として、NSItemProviderReading と NSItemProviderWriting があります。
NSItemProviderReading は、ドロップされたときに オブジェクトを構築するために使用されます。(ドロップ時に、NSItemProvider から オブジェクトを受け取れるようにします)
NSItemProviderWriting は、ドラッグするときに、オブジェクトを提供するために使用されます。(ドラッグ時に、NSItemProvider に オブジェクトを提供できるようにします)
# いずれも、実際のデータ受け渡しは、ドラッグ&ドロップ操作が実行された時になります。
ベースとするクラス
// (1)
final class MyDropData: NSObject {
// (2)
static let dragDropID = "public.utf8-plain-text"
// (3)
var str: String
init(_ str:String) {
self.str = str
}
}
- NSItemProviderReading, NSItemProviderWriting に準拠するためには、class であることが必要で、NSObject を継承していないといけません。
- UTType として、”public.utf8-plain-text” を利用しています。
独自定義のものを使うには追加手順が必要になるので省略のためです - String を保持するだけのクラスです。
このクラスに、NSItemProviderReading, NSItemProviderWriting を実装していきます。
エラー処理のために以下のような enum を定義していますが、強い関係はありません。
enum MyError: Error {
case unknownType
case generateError
}
NSItemProviderReading
定義しなければいけないものは2つです。「受け取ることができるタイプ」と「受け取ってと言われた時のデータ受け取り方法」です。
それぞれ、static var readableTypeIdentifiersForItemProvider と static func object(withItemProviderData data: Data, typeIdentifier: String) を定義することで設定します。
Apple のドキュメントは、こちら。
// (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
}
- readableTypeIdentifiersForItemProvider として、 “public.utf8-plain-text” を指定しています
- この関数が、ドロップされたときに、onDrop 内で ドラッグ&ドロップのデータからクラスを構築するために使用されるメソッドです。(static です)
- タイプをチェックして、想定していないタイプであればエラーとします
- 渡されるデータから String を復元し、MyDropData クラスのインスタンスを作成して返しています
オブジェクトの構築は、上記のように static メソッドの中で行われるため、必要な情報は全て、ドラッグ&ドロップ時のデータに含めておく必要があります。
NSItemProviderWriting
Reading と同様に定義しなければいけないものは2つです。「渡すことができるタイプ」と「渡してと言われた時にデータを渡す方法」です。
それぞれ、static var writableTypeIdentifiersForItemProvider と func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? を定義することで定義します。
Apple のドキュメントは、こちら。
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
}
- String を受け渡すために、String を Data 化しています
- completionHandler 経由で、Data を受け手に渡します
- Progress を返すことで途中でのキャンセル等にも対応させることが可能です
NSItemProviderReading/NSItemProviderWriting の有無で比較
機能的に差異はありませんが、コードを並べておきます。
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
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
}
テストコード全体
前回のコードに今回のコードを追加した最終形は以下の通りです。
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 から オブジェクトとして受け取ることが可能となる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link