SwiftUIでドラッグ&ドロップ
UIKitでドラッグ&ドロップいろんなことができていますが、SwiftUIではどこまでしょうか?
一緒に見ていきましょう。
ドラック
func onDrag(_ data: @escaping () -> NSItemProvider) -> some View
Modiferになっていますので、ドラッグのプレビューが適用されたビューになります。
onTapGesture(count:perform:)
と同じ理屈で、場合によってcontentShape(_:eoFill:)
で調整する必要があります。
view
.padding()
.contentShape(Rectangle()) // ないとパディングがドラッグのプレビューに反映されない
.onDrag { ... }
Modiferに提供するのは() -> NSItemProvider
のみで、クロージャーですので、どのビューがドラッグされるのを記録するのはちょうどいいように見えますが、ドラッグがキャンセルされた場合検知できませんので、記録できていてもリセットできません。現時点あくまでNSItemProvider
の作成がlazyになっているだけです。
NSItemProvider
の作成によく使われるイニシャライザはinit(object: NSItemProviderWriting)
で、NSString
やUIImage
などはすでにNSItemProviderWriting
を実装済みですので、直接使うことが可能です。
自作のモデルを渡せたい場合、NSItemProviderWriting
を実装する必要があります。すでにCodable
を実装したタイプでは割と簡単です。
extension SomeCodable: NSItemProviderWriting {
static var writableTypeIdentifiersForItemProvider: [String] {
["com.example.SomeCodable"]
}
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
do {
let data = try JSONEncoder().encode(self)
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
return nil
}
}
自作のtype identifierはuniform type identifier (UTI)の宣言を行う必要がありますので、info.plist
に下記の内容を追加し、宣言します。
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>com.example.SomeCodable</string>
</dict>
</array>
これでドラッグの準備ができました。
ドロップ
ドロップのmodifierは下記の三つになります。
-
func onDrop(of supportedContentTypes: [UTType], delegate: DropDelegate) -> some View
-
func onDrop(of supportedContentTypes: [UTType], isTargeted: Binding<Bool>?, perform action: @escaping ([NSItemProvider]) -> Bool) -> some View`
-
func onDrop(of supportedContentTypes: [UTType], isTargeted: Binding<Bool>?, perform action: @escaping ([NSItemProvider], CGPoint) -> Bool) -> some View`
DropDelegate
の方はいろいろ制御できます1が、perform action
の方もドロップした座標が取れますので、普通の場合は十分です。
.onDrop(of: [UTType(exportedAs: "com.example.SomeCodable")], // iOS 13は[String]
isTargeted: nil) { itemProviders, location -> Bool in
guard let itemProvider = itemProviders.first else {
return false
}
itemProvider.loadObject(ofClass: SomeCodable.self) { item, error in
guard let someCodable = item as? SomeCodable else {
return
}
...
}
return true
}
これでドロップできますが、loadObject(ofClass:completionHandler:)
を使うにはSomeCodable
がNSItemProviderReading
を実装する必要があります。
extension SomeCodable: NSItemProviderReading {
static var readableTypeIdentifiersForItemProvider: [String] {
["com.example.SomeCodable"]
}
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
try JSONDecoder().decode(Self.self, from: data)
}
}
NSItemProviderWriting
の逆ですね。
これにてドラッグ&ドロップができるようになりました。
問題点
UIKitのドラッグ&ドロップとの仕組みが違うようで、裏ではUIKitで描画するものにはドロップできない場合があります。私が気づいたのは下記になります。
TabView
のtabItem(_:)
List
(裏にあるUITableView
のdragInteractionEnabled
をtrue
にすれば一応ドラッグは可能だけど)
まとめ
UIKitの場合よりできることは少ないですが、その割に実装は結構簡単です。
- ドラッグさせたいビューに
.onDrag
- ドロップさせたいビューに
.onDrop
- 自作モデルの場合
NSItemProviderWriting
とNSItemProviderReading
を実装
参考
- macos - Drag and drop with custom type identifier doesn’t work - Stack Overflow
- Declaring New Uniform Type Identifiers
-
現時点
supportedContentTypes
とvalidateDrop(info:)
は機能していません。 ↩