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)で、NSStringUIImageなどはすでに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:)を使うにはSomeCodableNSItemProviderReadingを実装する必要があります。

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で描画するものにはドロップできない場合があります。私が気づいたのは下記になります。

  • TabViewtabItem(_:)
  • List(裏にあるUITableViewdragInteractionEnabledtrueにすれば一応ドラッグは可能だけど)

まとめ

UIKitの場合よりできることは少ないですが、その割に実装は結構簡単です。

  • ドラッグさせたいビューに.onDrag
  • ドロップさせたいビューに.onDrop
  • 自作モデルの場合NSItemProviderWritingNSItemProviderReadingを実装

参考


  1. 現時点supportedContentTypesvalidateDrop(info:)は機能していません。