【SwiftUI】iOSでDropDelegateのdropEnteredでloadObjectやloadItemが動作しない

DropDelegateについて

SwiftUIでDrag&Dropを実装する際、簡単な内容であればonDragとonDropで実装できます。

しかし、細かい動作を指定したい場合はDropDelegateを使用します。
中でもドラッグしたままドロップ先に重ねたタイミングで動作するのがdropEnteredです。

問題点

iOS16.2現在、dropEnteredでloadObjectやloadItemが動作しません。
ドラッグし終えたタイミングのperformDropでは動作します。
またmacOSで同じコードを実行させると動作します。

本来はDrag&Dropを実装する場合はNSItemProviderを通してデータのやり取りをします。
このNSItemProviderからデータを取り出すのがloadObjectやloadItemです。
これが動作しないということはドラッグし終えるまでデータが取得できないということになります。

とはいえ単一のアプリ内であればNSItemProviderを使用せずともデータをやり取りできます。
iOSとmacOSで異なる挙動をする事と、本来のNSItemProviderを使用した実装が出来ないのは少々気持ち悪くはありますが・・・。

アプリ間でDrag&Dropを実装する際はNSItemProviderを使用しないといけない為、
ドラッグ中にデータの使用が出来ず、ドロップするまで反映出来ない事に注意して下さい。

サンプルコード

適当なコードを用意しました。
SwiftUIのみのコードなのでそのままmacOSでも実行出来ます。

内容としてはFromをドラッグして重ねるとToの背景が青へ代わり、
ドロップするとToの背景が赤に変わります。

NSItemProviderの中身は特に使ってないのでprintで確認してるだけです。

Task{ await MainActor.run{ 〜 } }はloadObjectが非同期なので、
Viewの更新をメインスレッドで実行する為です。
DispatchQueue.main.async { 〜 }でも良いですし、最悪中のコードだけでも動きはします。

import SwiftUI
import UniformTypeIdentifiers

struct DragTestView: View {
    
    @State var color:Color = .green
    
    var body: some View {
        VStack{
            Text("From")
                .font(.headline)
                .frame(width: 160, height: 160)
                .background(Color.green)
                .onDrag {
                    return NSItemProvider(object: NSString("From"))
                }
            
            Text("To")
                .font(.headline)
                .frame(width: 160, height: 160)
                .background(color)
                .onDrop(of: [UTType.text], delegate: TestDropDelegate(color:$color))
            
            Button("Reset"){
                color = .green
            }
        }
    }
}

struct TestDropDelegate:DropDelegate{
    
    @Binding var color:Color
    
    func dropEntered(info: DropInfo) {
        guard let provider = info.itemProviders(for: [UTType.text]).first else {
            return
        }
        
        provider.loadObject(ofClass: NSString.self) { item, error in
            print(item)
            
            Task{
                await MainActor.run{
                    color = .blue
                }
            }
        }
        
    }
    
    func performDrop(info: DropInfo) -> Bool {
        guard let provider = info.itemProviders(for: [UTType.text]).first else {
            return false
        }
        
        provider.loadObject(ofClass: NSString.self) { item, error in
            print(item)
            
            Task{
                await MainActor.run{
                    color = .red
                }
            }
        }
        
        return true
    }
}

このパターンだと先に述べたようにNSItemProvider無しで動作できるので特に問題はありません。
dropEnteredでloadObjectを使わずに実行すれば良いだけです。

データの受け渡しが必要になってもアプリ内なので適当に変数を使えば十分ですね。

最後に

iOSのdropEnteredでloadObjectやloadItemが動作しませんでした。

特にどこかに説明があるわけではありませんが、
macOSとiOSで挙動が異なるので気をつけて下さい。

Drag&Dropのコードを探すとNSItemProviderが使用されていないものが多いのはこれが原因かもしれません。

しかし、NSItemProviderを使用すると対応するデータ型を受け取るようにすると、
アプリ間でのDrag&Dropが可能になります。

performDropでは問題なく動作するので、
可能な限りNSItemProviderを使用するようにした方が良いです。

コメント

タイトルとURLをコピーしました