Sponsor Link
環境&対象
- macOS Big Sur 11.3beta
- Xcode 12.4
- iOS 14.4
onMove の代替策
以下の記事でも説明していますが、SwiftUI の List/ForEach の onMove を使うと、気をつけないとすぐにクラッシュしてしまうことがあります。
[SwiftUI] List/ForEach の onMove についての メモ書き
だったら、自分で実装する方が良いのではないかということで、自分で実装してみたのですが、驚くほど簡単だったので、説明します。
# SwiftUI と Drag&Drop の仕組みの理解は必要です。
リスト表示のアプリ
まずは、ドラッグ&ドロップ対象となるアプリが必要です。
以下のように、テキストをリスト表示するアプリにしました。
struct ContentView: View {
@State var data = ["Item0", "Item1", "Item2", "Item3", "Item4"]
var body: some View {
List {
ForEach(data, id: \.self) { element in
Text(element)
.frame(height:20)
}
.padding()
}
}
}

onMove の分析
実装を開始する前に、onMove の機能を考えてみました。
func onMove(perform action: Optional(IndexSet, Int) -> Void>) -> some DynamicViewContent
リスト中の要素をドラッグした時に、要素間にインジケータを表示しドロップされた時には、onMove を呼び出します。引数の IndexSet がドラッグした要素のインデックス、Int が ドロップ先のインデックスになっています。
例題のケースで Item1 を Item3 の後にドロップすると、IndexSet には、1 が入っていて、Int には、4 が渡されてきます。
# この indexSet と int の組み合わせは配列等に適用できる move の引数と同じです。
# この move は、SwiftUI を import しないと使えません。
ですので、onMove は、リスト中の指定位置の要素を、別の指定位置に配置するための UI を作ってくれています。実際のデータ操作は、onMove の中に実装する必要があります。
少し分解すると、「指定位置の要素からのドラッグ開始」と「リスト要素間へのドロップ」を行ってくれるということです。
指定位置の要素からのドラッグ開始
つまり、要素からドラッグ開始ができるようにすれば良いということです。List/ForEach 中の要素に onDrag を使うことで、実現できます。
リスト要素間へのドロップ
これは、ForEach の onInsert を使うことで実現できます。
つまり、onMove = onDrag + onInsert ということです。
ドラッグ&ドロップによるリスト要素移動の実装
きちんと UTI を登録して、UTI による ドラッグ&ドロップとして実装していきます。
UTI 定義
以下のように定義しました。
extension UTType {
static var ddType: UTType {
UTType(exportedAs: "com.smalldesksoftware.ddType", conformingTo: UTType.item)
}
}
コード上での定義だけでなく、Info.plist にも登録します。
DragDropItem の定義
NSItemProvider で扱う オブジェクトを定義します。上で定義した UTI の読み書きに対応しているオブジェクトとして定義します。
final class MyDragDropData: NSObject, NSItemProviderReading, NSItemProviderWriting {
static let dragDropID = UTType.ddType
// data container
var str: String
init(_ str:String) {
self.str = str
}
static var readableTypeIdentifiersForItemProvider: [String] { [ Self.dragDropID.identifier ] }
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> MyDragDropData {
guard typeIdentifier == Self.dragDropID.identifier else { throw MyError.generalError }
if let str = String(data: data, encoding: .utf8) {
return MyDragDropData(str)
}
throw MyError.generateError
}
static var writableTypeIdentifiersForItemProvider: [String] { [ Self.dragDropID.identifier ] }
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
let data = str.data(using: .utf8)
completionHandler(data, nil)
return nil
}
}
ドラッグ&ドロップは、NSItemProvider 経由でこのオブジェクトでのやりとりとなります。
実装していることは、プロパティの str の Data への変換とその逆です。
サンプルアプリの拡張
onMove = onDrag + onInsert だったので、Text に onDrag を付与し、ForEach に onInsert を指定していきます。
struct ContentView: View {
@State var data = ["Item0", "Item1", "Item2", "Item3", "Item4"]
var body: some View {
List {
ForEach(data, id: \.self) { element in
Text(element)
.frame(height:20)
.onDrag {
// (1)
NSItemProvider(object: MyDragDropData(element))
}
}
.onInsert(of: [MyDragDropData.dragDropID], perform: { index, providers in
// (2)
guard let provider = providers.first else { return }
guard provider.canLoadObject(ofClass: MyDragDropData.self) else { return }
provider.loadObject(ofClass: MyDragDropData.self) { (data, error) in
if let myDropData = data as? MyDragDropData {
// (3)
guard let sourceIndex = self.data.firstIndex(of: myDropData.str) else { return }
// (4)
self.data.move(fromOffsets: IndexSet(integer: sourceIndex), toOffset: index)
}
}
})
.padding()
}
}
}
- ドラッグ時に、ドラッグされる要素(String です) から MyDragDropData を作成し、NSItemProvider に渡します
- 指定した UTI 以外のドロップは受け付けないはずですが、念の為チェックしてます
- ドラッグ時に渡した MyDragDropData を復元し、データからどの要素がドラッグされたかを探します
- data (String の Array) の順序を入れ替えます
MyDragDropData に ドラッグ元のインデックスを入れる方法もありますが、今回はデータからインデックスを検索してます。
実装したアプリの動き
機能的には onMove と同様に動きます。
# 2回目のドロップから ドロップインディケータが表示されなくなります。(SwiftUI の バグな気がします)
まとめ:SwiftUI の List/ForEach の onMove を自作する方法
- onInsert, onDrag を使って、実装する
- List 毎に異なる UTI を使用すると、List 間の ドラッグ&ドロップを制御しやすい
- onInsert, onDrag を使うと、ドロップインディケータの動きが少し怪しい・・・(on macOS)
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link