[SwiftUI] onMove を自分で実装する

SwiftUI

SwiftUI の List で onMove を安易に使うとハマるので、自作してみました

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3beta
  • Xcode 12.4
  • iOS 14.4

onMove の代替策

以下の記事でも説明していますが、SwiftUI の List/ForEach の onMove を使うと、気をつけないとすぐにクラッシュしてしまうことがあります。

SwiftUI[SwiftUI] List/ForEach の onMove についての メモ書き

だったら、自分で実装する方が良いのではないかということで、自分で実装してみたのですが、驚くほど簡単だったので、説明します。

# SwiftUI と Drag&Drop の仕組みの理解は必要です。

リスト表示のアプリ

まずは、ドラッグ&ドロップ対象となるアプリが必要です。

以下のように、テキストをリスト表示するアプリにしました。

List 表示アプリ

List App
List App

onMove の分析

実装を開始する前に、onMove の機能を考えてみました。

onMove

リスト中の要素をドラッグした時に、要素間にインジケータを表示しドロップされた時には、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 定義

以下のように定義しました。

UTType 定義

コード上での定義だけでなく、Info.plist にも登録します。

DragDropItem の定義

NSItemProvider で扱う オブジェクトを定義します。上で定義した UTI の読み書きに対応しているオブジェクトとして定義します。

MyDragDropData

ドラッグ&ドロップは、NSItemProvider 経由でこのオブジェクトでのやりとりとなります。

実装していることは、プロパティの str の Data への変換とその逆です。

サンプルアプリの拡張

onMove = onDrag + onInsert だったので、Text に onDrag を付与し、ForEach に onInsert を指定していきます。

List 表示アプリ(ドラッグ&ドロップ対応)
コード解説
  1. ドラッグ時に、ドラッグされる要素(String です) から MyDragDropData を作成し、NSItemProvider に渡します
  2. 指定した UTI 以外のドロップは受け付けないはずですが、念の為チェックしてます
  3. ドラッグ時に渡した MyDragDropData を復元し、データからどの要素がドラッグされたかを探します
  4. data (String の Array) の順序を入れ替えます
MEMO
MyDragDropData に ドラッグ元のインデックスを入れる方法もありますが、今回はデータからインデックスを検索してます。

実装したアプリの動き

機能的には onMove と同様に動きます。

# 2回目のドロップから ドロップインディケータが表示されなくなります。(SwiftUI の バグな気がします)

まとめ:SwiftUI の List/ForEach の onMove を自作する方法

SwiftUI の List/ForEach の onMove を自作する方法
  • onInsert, onDrag を使って、実装する
  • List 毎に異なる UTI を使用すると、List 間の ドラッグ&ドロップを制御しやすい
  • onInsert, onDrag を使うと、ドロップインディケータの動きが少し怪しい・・・(on macOS)

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です