
SwiftUIでドラッグ&ドロップの挙動を細かくカスタマイズしたいときに活躍するのが DropDelegate
です。
特定のビューにドロップ処理を割り当てたり、ドラッグ対象を受け取った位置で並び替えを行ったりといった高度な操作が可能になります。
この記事では、DropDelegate
の基本的な使い方から、実装に必要なメソッド、代表的な活用シーン、注意点までをわかりやすく丁寧に解説します。
DropDelegateとは?
DropDelegate
は、SwiftUIにおいてドラッグ&ドロップでアイテムがビューに「ドロップ」されたときの挙動を定義するためのプロトコルです。
ドロップ位置の検出、要素の移動、ドロップの受け入れ条件など、細かな動きをハンドリングしたいときに使います。
標準の .onDrop()
モディファイアでも簡易的なドロップは可能ですが、位置依存のロジックや並び替えなどを行いたい場合には DropDelegate
が必要になります。
基本的な使い方
DropDelegate
を使う時は、.onDrop(of:delegate:)
に DropDelegate に準拠した型 を渡す必要があります。
つまり「DropDelegate を実装した構造体」をあらかじめ用意しておき、それを delegate
引数に指定して使う、という流れになります。
具体例①:ビューに DropDelegate を割り当てる
例えば、リスト内でアイテムをドラッグして並び替えるようなUIを作る場合、各行に DropDelegate
を割り当てて順序を入れ替えられるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct ReorderableRow: View { let item: Item let currentIndex: Int @Binding var items: [Item] @State private var draggingItem: Item? var body: some View { Text(item.name) .onDrag { self.draggingItem = item return NSItemProvider(object: item.name as NSString) } .onDrop(of: [.text], delegate: ItemDropDelegate( item: item, items: $items, currentIndex: currentIndex, draggingItem: $draggingItem )) } } |
ここでは、ドラッグ開始時に item
を記録し、それを ItemDropDelegate
(DropDelegate に準拠した型) に渡しています(どこで指定しているかは、次の章で説明します)
どの要素をどこにドロップするかの制御は、この ItemDropDelegate
の中で行われます。
具体例②:DropDelegate の実装
DropDelegate
を使うには、独自の構造体にこのプロトコルを適用し、いくつかのメソッドを実装します。
上の例で登場した ItemDropDelegate
の中身は次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
struct ItemDropDelegate: DropDelegate { let item: Item @Binding var items: [Item] let currentIndex: Int @Binding var draggingItem: Item? func performDrop(info: DropInfo) -> Bool { draggingItem = nil return true } func dropEntered(info: DropInfo) { guard let draggingItem, draggingItem != item else { return } if let fromIndex = items.firstIndex(of: draggingItem), let toIndex = items.firstIndex(of: item) { withAnimation { items.move( fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex ) } } } func dropUpdated(info: DropInfo) -> DropProposal? { DropProposal(operation: .move) } } |
この ItemDropDelegate
は、リストの要素をドラッグして別の位置に ドロップ(配置) したときに、リストの並び順を変更する役割を持ちます。
プロパティ
item
: 現在の行の要素(ドロップ先を表す)items
: リスト全体(@Binding なので親ビューと同期する)currentIndex
: 今の行のインデックス(位置)draggingItem
: 現在ドラッグしているアイテム(@Binding で共有)
メソッド
performDrop(info:)
- ドロップが完了したときに呼ばれます。
- ここでは
draggingItem = nil
として、ドラッグが終了したことを記録しています。 true
を返すことで「ドロップ処理が成功した」と通知します。
dropEntered(info:)
- ドラッグ中のアイテムが、別の要素の上に「入った瞬間」に呼ばれます。
- もし今ドラッグしているアイテムが
item
(この行の要素)と異なる場合のみ処理を続けます。 fromIndex
(ドラッグ元の位置)とtoIndex
(ドロップ先の位置)を調べて、items.move(...)
を呼び出して配列内の順序を入れ替えます。withAnimation { ... }
によってアニメーション付きで並び替えが行われます。
つまり、リストの要素を別の場所に動かしたときに順序を変える処理 がここに書かれています。
dropUpdated(info:)
- ドロップ操作が進行中に位置が更新されたときに呼ばれます。
- ここでは
DropProposal(operation: .move)
を返して「これは移動操作です」とシステムに伝えています。 - これにより、ユーザーがドラッグしているときに「移動用の見た目」が適切に表示されます。
よく使われるメソッド
DropDelegate
で実装できる代表的なメソッドを、役割・呼ばれるタイミング・必須かどうかでまとめました。
メソッド名 | 呼ばれるタイミング | 役割 / 処理内容 | 必須かどうか |
---|---|---|---|
performDrop(info:) -> Bool |
ドロップ操作が完了したとき | 実際のドロップ処理を行う。 アイテムを受け入れた後の後始末など。戻り値 true で成功を示す |
必須 |
dropEntered(info:) |
ドラッグ中のアイテムがドロップ領域に入った瞬間 | ドラッグ対象を検知し、並び替えやハイライトなどの処理を行う | 任意 |
dropUpdated(info:) -> DropProposal? |
ドラッグ中にカーソル位置が更新されたとき | このドロップが「移動」「コピー」などどの操作になるかを提案する | 任意 |
dropExited(info:) |
ドラッグ対象がドロップ領域から外れたとき | ハイライトを解除する、仮表示を消すなど | 任意 |
validateDrop(info:) -> Bool |
ドロップ対象を受け入れてよいか判定するとき | ドロップ可能かどうかを返す。 特定の型・条件のデータだけ受け入れるときに使用 |
任意 |
performDrop(info:)
は必須- このメソッドを実装しないと、ドロップ時に「何をするか」が決まらないため、最低限これだけは必要です。
- よく使うのは
dropEntered(info:)
とdropUpdated(info:)
- 並び替え処理や見た目のフィードバックを実装するのに使います。
- 条件付きドロップを作りたいなら
validateDrop(info:)
- 例えば「画像だけ受け入れる」「特定のアイテムだけ並べ替え可能」にしたいときに便利です。
まとめると、必須の performDrop
をベースにして、必要に応じて dropEntered
/ dropUpdated
/ validateDrop
を追加していくイメージです。
DropDelegateの活用シーン
DropDelegate
が真価を発揮するのは、以下のような「位置依存」「並び替え」「複数データの受け入れ」などの場面です。
- リスト内でのアイテムの並べ替え
- カスタムビューへのドラッグ&ドロップでアイテム追加
- 条件付きでドロップを受け入れるフィルタリング
- ドラッグ対象の情報を保持したまま、複数のビュー間で状態を共有
例えば、リマインダーアプリやタスク管理アプリで「ドラッグして順序を変える」「カテゴリごとにドロップ」などの操作をしたい場合には必須となります。
注意点
DropDelegate を使う際にはいくつか注意が必要です。
onDrop(of:delegate:)
のof:
には UTType 配列を指定する必要があります(例:[.text]
)- DropDelegate は単体で動作するのではなく、onDrop(of:delegate:) モディファイアを通じてビューに適用されます。
- ここで指定する UTType が正しくないとドロップ自体が検知されません。
- DropDelegate の状態管理には
@Binding
を使って親 View と同期させる必要がありますDropDelegate
内で配列操作や並び替えを行っても、@Binding
を介して親ビューのデータと同期しなければ UI が更新されません。@State
や@Binding
を適切に組み合わせることが正しい挙動の鍵になります。
- 同一要素に複数のドロップを許可する場合、意図しない移動が起こることがあるためロジックに注意
- 複数のドロップを許可した場合に「どこに挿入されるか」が曖昧になり、要素の重複や意図しない移動が発生することがあります。
- そのため、ドロップを受け入れる条件や挙動をロジックで明確に定義しておくことが重要です。
まとめ
今回はSwiftUIの DropDelegate
について詳しく解説しました。
DropDelegate
はドロップ操作のカスタマイズを可能にするプロトコルonDrop(of:delegate:)
と組み合わせて使う- リスト内の並べ替えなど、ドラッグ&ドロップの位置依存制御に最適
dropEntered
,performDrop
,dropUpdated
などのメソッドを適切に使い分ける- 状態管理には
@Binding
と@State
を活用し、親子間の同期が重要
ドラッグ&ドロップを取り入れることで、より直感的でユーザーが使いやすいUIを実現できます。
DropDelegate
を使いこなして、動きのあるSwiftUIアプリを構築してみましょう。