[SwiftUI] Transferableを使ったDrag and Dropの実装方法

# はじめに
iOS16から使えるようになった`Transferable`を実装することでドラッグアンドドロップの実装が簡単になったようなので、調べて見ました。

# 環境
Xcode15.4
iOS16 ~

# 内容
画面に二つの領域を設定して、動物のアイコンが書かれたカードを自由にドラッグして移動できるように実装しました。
また、ドロップできる領域に入ったら赤い枠を表示するようにしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/7b64a71e-8e5b-4bf4-dcd4-b9d8839e09ae.png" width="200"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/1910d1b1-7ea6-9e8c-2c0d-4a118bbde17f.png" width="200"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/fc15d872-4f0d-7a82-28ed-aff8deb5b424.png" width="200">

SwiftUIでのドラッグアンドドロップの実装には以下3つの要素の理解が必要です。

- [`Transferable`](https://developer.apple.com/documentation/coretransferable/transferable)
オブジェクトをドラッグアンドドロップ操作で転送可能にするためのプロトコル。`Transferable`に準拠することで、オブジェクトはシステム全体で一貫した方法で転送されることが保証される。

- [`draggable(_:preview:)`](https://developer.apple.com/documentation/swiftui/view/draggable(_:preview:))
Viewをドラッグ可能にする。プレビューパラメータを使用して、ドラッグ中に表示されるカスタムプレビューを定義することもできる。

- [`dropDestination(for:action:isTargeted:)`](https://developer.apple.com/documentation/swiftui/view/dropdestination(for:action:istargeted:))
Viewをドロップ先として設定する。ドロップされたアイテムの処理方法と、ドロップターゲットがアクティブかどうかを示すフィードバックも受け取れる。


```swift: DragAndDropView
struct DragAndDropView: View {
    @State private var topAnimals: [Animal] = []
    @State private var bottomAnimals: [Animal] = Animal.sampleAnimals

    @State private var isDropTargetedOnTOp = false
    @State private var isDropTargetedOnBottom = false

    var body: some View {
        VStack {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
                ForEach(topAnimals, id: \.id) { animal in
                    AnimalView(animal: animal)
                        .draggable(animal) {
                            AnimalView(animal: animal)
                                .bold()
                                .foregroundStyle(Color.red)
                        }
                }
            }
            .padding(20)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.gray.opacity(0.2))
            .border(.red, width: isDropTargetedOnTOp ? 10 : .zero)
            .dropDestination(for: Animal.self) { droppedAnimals, _ in
                moveAnimals(droppedAnimals, from: &bottomAnimals, to: &topAnimals)
                return true
            } isTargeted: {
                isDropTargetedOnTOp = $0
            }

            Divider()

            LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
                ForEach(bottomAnimals, id: \.id) { animal in
                    AnimalView(animal: animal)
                        .draggable(animal) {
                            AnimalView(animal: animal)
                                .bold()
                                .foregroundStyle(Color.red)
                        }
                }
            }
            .padding(20)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.blue.opacity(0.1))
            .border(.red, width: isDropTargetedOnBottom ? 10 : .zero)
            .dropDestination(for: Animal.self) { droppedAnimals, _ in
                moveAnimals(droppedAnimals, from: &topAnimals, to: &bottomAnimals)
                return true
            } isTargeted: {
                isDropTargetedOnBottom = $0
            }
        }
        .padding()
    }

    private struct AnimalView: View {
        let animal: Animal

        var body: some View {
            VStack {
                Circle()
                    .fill(animal.animalColor)
                    .frame(width: 60, height: 60)
                    .overlay(Text(Animal.getAnimalIcon(for: animal.name)))
                Text(animal.name)
                    .font(.caption)
            }
            .padding(8)
            .background(Color.white)
            .cornerRadius(8)
            .shadow(radius: 2)
        }
    }

    private func moveAnimals(_ animals: [Animal], from source: inout [Animal], to destination: inout [Animal]) {
        for animal in animals {
            if let index = source.firstIndex(where: { $0.id == animal.id }) {
                source.remove(at: index)
                destination.append(animal)
            }
        }
    }
}
```

```swift:Animal
struct Animal: Codable, Transferable, Identifiable {
    let id: UUID
    let colorIndex: Int
    let name: String

    init(id: UUID = UUID(), colorIndex: Int, name: String) {
        self.id = id
        self.colorIndex = colorIndex
        self.name = name
    }

    static var transferRepresentation: some TransferRepresentation {
        CodableRepresentation(for: Animal.self, contentType: .data)
    }
}
```

ドラッグするアイテムを`Transferable`に準拠させるには、`transferRepresentation`を実装して[`CodableRepresentation`](https://developer.apple.com/documentation/coretransferable/transferable)な値を返す必要があります。その際`contentType`を指定する必要があります。

今回は`.data`を使いましたが、カスタム`UTType`を追加することもできます。

https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/data

Viewに`.draggable(animal)`をつけるだけでもドラッグすることはできます。
ドラッグしている間にViewを変えたい場合は、previewにViewを追加します。
この実装ではドラッグ中のタイトルを変えています。

```swift
AnimalView(animal: animal)
    .draggable(animal) {
        AnimalView(animal: animal)
            .bold()
            .foregroundStyle(Color.red)
    }
```

ドロップ対象のViewに`.dropDestination`をつけるとアイテムをドロップすることができます。
`isTargeted`を使うことでドラッグ中にドロップ可能かどうかをUIでフィードバックすることができます。

```swift
    .dropDestination(for: Animal.self) { droppedAnimals, _ in
        moveAnimals(droppedAnimals, from: &bottomAnimals, to: &topAnimals)
        return true
    } isTargeted: {
        isDropTargetedOnTOp = $0
    }
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/5afd4f30-2cda-e34a-7678-3ad72b53f020.png" width="200">

# おわりに
`Transferable`を使うことで2つの異なるアプリ間でもドラッグアンドドロップでアイテムを動かすことができるようです。アプリ間移動が別で調べてみようかと思います。

ちなみに、`Transferable`は`PhotosPicker`でも使っていました。こちらの記事は`PhotosPicker`の使い方をまとめたものです。

https://qiita.com/yoshitaka/items/d99d89b3e19216fb4c2a

# 参考

https://www.hackingwithswift.com/quick-start/swiftui/how-to-support-drag-and-drop-in-swiftui

https://developer.apple.com/documentation/coretransferable/choosing-a-transfer-representation-for-a-model-type

https://zenn.dev/zunda_pixel/articles/3e9871a18d3b01