SwiftUIで，ドラッグアンドドロップを実装したい

# はじめに
Advent Calendar 2024として記事を書くことになったので最近やってみたくなったドラッグアンドドロップについてしらべて,まとめてみようと思います．個人的なまとめを，記事として書きます.
# OnDragとOnDropの機能について
ささっと調べてみると，OnDragとOnDropの機能を用いることで，簡易的なものを作成できるっぽいので，はじめに挑戦してみます．
``` Swift: sample1
import SwiftUI
struct sample1: View {
    @State private var items = ["Item 1", "Item 2", "Item 3"]

    var body: some View {
        VStack {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .padding()
                    .background(Color.blue.opacity(0.2))
                    .cornerRadius(8)
                    .onDrag {
                        NSItemProvider(object: item as NSString)
                    }
            }
            .onDrop(of: [.text], delegate: DropViewDelegate(items: $items))
        }
    }
}

struct DropViewDelegate: DropDelegate {
    @Binding var items: [String]

    func performDrop(info: DropInfo) -> Bool {
        guard let item = info.itemProviders(for: [.text]).first else { return false }
        item.loadItem(forTypeIdentifier: "public.text", options: nil) { (data, error) in
            if let data = data as? Data, let text = String(data: data, encoding: .utf8) {
                DispatchQueue.main.async {
                    items.append(text)
                }
            }
        }
        return true
    }
}
```
onDrag→ドラッグ中のデータを指定
onDrop→ドロップ先の処理を実装
今回だとText(item)をドラッグできるように設定し，ForEachにドロップすると，同じテキスト名のものを下に追加するというコードになっている．


<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3913811/1440ca04-1a2b-fc10-91d9-cd02989c9e89.png">
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3913811/699fdf00-6086-81b1-5fc2-0142dda2757b.png">

# 画像のドラッグアンドドロップ
次に、画像を使ったドラッグ&ドロップの機能について挑戦しました。この例では、ユーザーが画像をドラッグして正方形のエリア間で移動させたり、画像選択ボタンを使って画像を選択できるようにしています。具体的なコードは以下の通りです。
```Swift:sample2
struct ContentView: View {
    @State private var isPickerPresented = false
    @State private var selectedImage: UIImage?
    @State private var topImage: UIImage?
    @State private var bottomImage: UIImage?

    var body: some View {
        VStack(spacing: 20) {
            ZStack {
                Rectangle()
                    .strokeBorder(Color.gray, lineWidth: 2)
                    .frame(width: 300, height: 300)
                    .background(Color(.systemGray6))

                if let image = topImage {
                    Image(uiImage: image)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 300, height: 300)
                        .onDrag {
                            NSItemProvider(object: image)
                        }
                } else {
                    Text("ここに画像をドラッグ")
                        .foregroundColor(.gray)
                        .multilineTextAlignment(.center)
                }
            }
            .onDrop(of: [.image], isTargeted: nil) { providers in
                handleTopDrop(providers: providers)
            }
            ZStack {
                Rectangle()
                    .strokeBorder(Color.gray, lineWidth: 2)
                    .frame(width: 300, height: 300)
                    .background(Color(.systemGray6))

                if let image = bottomImage ?? selectedImage {
                    Image(uiImage: image)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 300, height: 300)
                        .onDrag {
                            NSItemProvider(object: image)
                        }
                } else {
                    Text("ここに画像をドラッグ\nまたは\nボタンを押して選択")
                        .foregroundColor(.gray)
                        .multilineTextAlignment(.center)
                }
            }
            .onDrop(of: [.image], isTargeted: nil) { providers in
                handleBottomDrop(providers: providers)
            }

            Button(action: {
                isPickerPresented = true
            }) {
                Text("画像を選択")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .sheet(isPresented: $isPickerPresented) {
            PHPickerViewControllerWrapper(selectedImage: $selectedImage, bottomImage: $bottomImage)
        }
        .padding()
    }

    private func handleTopDrop(providers: [NSItemProvider]) -> Bool {
        for provider in providers {
            if provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { object, _ in
                    DispatchQueue.main.async {
                        if let image = object as? UIImage {
                            self.topImage = image
                        }
                    }
                }
                return true
            }
        }
        return false
    }

    private func handleBottomDrop(providers: [NSItemProvider]) -> Bool {
        for provider in providers {
            if provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { object, _ in
                    DispatchQueue.main.async {
                        if let image = object as? UIImage {
                            self.bottomImage = image
                        }
                    }
                }
                return true
            }
        }
        return false
    }
}
```
写真を下側の正方形に表示し，それを上の正方形にドラッグアンドドロップで移動させることができます．(画像選択のコードは省略)
<img width="180" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3913811/5ad5249f-f7ed-8629-9095-1b7c44592d39.png"><img width="180" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3913811/a0333edd-a289-7c21-9bfb-61afcbab9bf2.png"><img width="180" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3913811/4475d714-80cc-e431-a9f5-53f775254da2.png">

# ドラッグアンドドロップで並び替え
最後に，リストの並び替えをドラッグアンドドロップで実現する方法について触れます。SwiftUIで，簡単にこの機能を実装できました！！
``` Swift:sample3
struct sample3: View {
    @State private var items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]

    var body: some View {
        NavigationView {
            List {
                ForEach(items, id: \.self) { item in
                    Text(item)
                }
                .onMove(perform: moveItem)
            }
            .navigationTitle("Drag & Drop List")
        }
    }

    private func moveItem(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
}
```
このコードでは、ForEachに対して.onMoveを追加し，並び替えの操作を可能にしている．リスト内のアイテムをドラッグすると，moveItemメソッドが呼び出されitems配列内で順序を変更している．
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3913811/34ef0c37-f990-6a63-fe7d-a03da200538c.png"><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3913811/799c69a6-23e1-82bd-476b-1e1afcd6c6d3.png">

# 最後に
ドラッグ&ドロップについて，それなりに理解できたので，今後の開発で，この機能を取り入れていきたいです！！また，今後の展望として，アプリ間を移動するようなドラッグ&ドロップも挑戦してみようかなと思います．
今後も開発頑張りまーす👍