【SwiftUI】Grid内のImageをDrag&Dropで移動させる方法
今回紹介したい内容
今回はLazyVGridやLazyHGridで配置したImageをdrag&dropで移動させる方法を試しに作っていきたいと思います。
環境
・ macOS: Monterey
・ Xcode: 13.3
・ iOS: 15.4
実装
準備
初期の状態として以下のViewを用意します。
Grid表示については以前に以下のような記事を書きましたので参考にしていただけると幸いです。
struct GridTestView: View {
@ObservedObject var viewMdoel = ItemViewModel()
let columns = Array(repeating: GridItem(.flexible(), spacing: 20), count: 2)
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(viewMdoel.items) { index in
ZStack {
Image(index.item)
.resizable()
.frame(width: 150, height: 150)
}
}
}
}
}
}
Model側にIdentifiableに準拠したstructを用意します。
struct Item: Identifiable {
var id = UUID().uuidString
var item: String
}
ViewModel側には以下を用意します。
class ItemViewModel: ObservableObject {
@Published var cuurentItem: Item?
@Published var items = [
Item(item: "1"),
Item(item: "2"),
Item(item: "3"),
Item(item: "4"),
Item(item: "5"),
Item(item: "6"),
Item(item: "7"),
Item(item: "8"),
Item(item: "9"),
Item(item: "10"),
Item(item: "11"),
Item(item: "12"),
Item(item: "13")
]
}
Assetsに1〜13の名前でImageを入れています。
(適当にO-DANなどで用意していください)
完成イメージ
DropDelegateの追加
struct DropViewDelegate: DropDelegate {
var item: Item
var viewModel: ItemViewModel
func performDrop(info: DropInfo) -> Bool {
return true
}
func dropEntered(info: DropInfo) {
//from
let fromIndex = viewModel.items.firstIndex { (item) -> Bool in
return item.id == viewModel.cuurentItem?.id
} ?? 0
//to
let toIndex = viewModel.items.firstIndex { (item) -> Bool in
return item.id == self.item.id
} ?? 0
if fromIndex != toIndex {
withAnimation(.default) {
let formPage = viewModel.items[fromIndex]
viewModel.items[fromIndex] = viewModel.items[toIndex]
viewModel.items[toIndex] = formPage
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
それぞれ何をしているメソッドなのか以下のドキュメントを見ていただければわかると思いますのでリンク貼っておきます。
一番重要な部分はdropEnteredの部分かと個人的に思っていますので少し説明しておきます。
ここではドロップ操作がされ終えた事をデリゲートに通知します。(筆者はそう解釈しています)その通知のタイミングでfromIndexとtoIndexを用意して位置を変更しています。後に記述しますが、ここを変更する事でGrid表示されているImageを移動させた際に、他のImageの移動の仕方が変わります。
これでドロップした際にImageを移動させることができるのでViewにonDragとonDropを反映していきます。
Viewの全体のコードは以下のようになります。
struct GridTestView: View {
@ObservedObject var viewMdoel = ItemViewModel()
let columns = Array(repeating: GridItem(.flexible(), spacing: 20), count: 2)
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(viewMdoel.items) { index in
ZStack {
Image(index.item)
.resizable()
.frame(width: 150, height: 150)
}
.onDrag {
viewMdoel.cuurentItem = index
return NSItemProvider(contentsOf: URL(string: "\(index.id)")!)!
}
.onDrop(of: [.url], delegate:
DropViewDelegate(item: index,
viewModel: viewMdoel))
}
}
}
}
}
onDragではドラッグアンドドロップまたはコピー/貼り付けアクティビティ中にプロセス間で、またはホストアプリからアプリ拡張機能にデータまたはファイルを伝達するためのアイテムプロバイダーとしてNSItemProviderを使用します。
こちらもドキュメントリンクを貼っておきます。
その他Imageの移動の仕方を変更する
以下のようにその他のImageの移動の仕方を変更することも簡単にできます。
dropEntered内にある以下の部分を
let formPage = viewModel.items[fromIndex]
viewModel.items[fromIndex] = viewModel.items[toIndex]
viewModel.items[toIndex] = formPage
以下のように変更するだけです。
viewModel.items.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
まとめ
後は細かい部分でDragのタイミングとDropのタイミングで移動させるImageのopacityを変更するなどするともっと滑らかな表現ができるのかなと感じました。
今回の記事で参考にしたドキュメントは以下に貼っておきます。Dragジェスチャーを利用してハーフモーダルなどは作ったりはしてきましたが、今回DropDelegateやNSItemProviderを初めて使用し、バイトストリームを内部で行なっていることにも少し触れることができて良かったです。
またもっとライトに試すのであればImageではなく適当に図形用意して、試すこともできますので気になる方は是非試してみてください。
参考
Discussion