[SwiftUI]ドラッグ&ドロップによる並び替えと値を渡しながらモーダル遷移

# はじめに
下記を実装しました。
- Transferableを利用してドラッグアンドドロップで並び替える
- タップで値を渡しながらモーダル遷移をする

![drag&drop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1015674/60d4939f-9711-d7e8-19de-410026eba6bd.gif)

# 環境
Xcode16.0
iOS18

# ドラッグ&ドロップで配置入れ替え
[**Transferable**](https://developer.apple.com/documentation/coretransferable/transferable)
[**draggable(_:preview:)**](https://developer.apple.com/documentation/assignables/assignabledocumentview/draggable(_:preview:)/)
[**dropDestination(for:action:)**](https://developer.apple.com/documentation/swiftui/view/dropdestination(for:action:istargeted:))

```swift
import SwiftUI

struct ContentView: View {
  
  @State private var sampleItemList: [Item] = [
    .init(id: 1, title: "あ"),
    .init(id: 2, title: "い"),
    .init(id: 3, title: "う"),
    .init(id: 4, title: "え"),
    .init(id: 5, title: "お"),
    .init(id: 6, title: "か"),
    .init(id: 7, title: "き"),
    .init(id: 8, title: "く"),
    .init(id: 9, title: "け"),
  ]
  
  @State private var draggingItem: Item?
  @State private var selectedItem: Item? = nil
  
  var body: some View {
    NavigationStack {
      GeometryReader{ geometry in
        let itemSize = max((geometry.size.width - 40) / 3, 0)
        let columns = Array(repeating: GridItem(spacing: 20), count: 3)
        LazyVGrid(columns: columns, spacing: 20, content: {
          ForEach(sampleItemList, id: \.self.id) { sampleItem in
            RoundedRectangle(cornerRadius: 10)
              .stroke(Color.orange, lineWidth: 6)
              .background(RoundedRectangle(cornerRadius: 10).fill(Color.orange.opacity(0.1)))
              .frame(width: itemSize, height: itemSize)
              .overlay(
                HStack{
                  Text("\(sampleItem.id)")
                    .font(.system(size: itemSize / 2.5))
                    .fontWeight(.bold)
                    .foregroundColor(.black)
                  Text(sampleItem.title)
                    .font(.system(size: itemSize / 2.5))
                    .fontWeight(.bold)
                    .foregroundColor(.black)
                }
                  .padding()
              )
              .onTapGesture {
                selectedItem = sampleItem
              }
              .draggable(sampleItem) {
                RoundedRectangle(cornerRadius: 10)
                  .fill(.ultraThinMaterial)
                  .frame(width: itemSize, height: itemSize)
                  .onAppear{
                    draggingItem = sampleItem
                  }
              }
              .dropDestination(for: Item.self) { items, location in
                draggingItem = nil
                return false
              } isTargeted: { status in
                if let draggingItem, status, draggingItem != sampleItem  {
                  if let draggingIndex = sampleItemList.firstIndex(of:draggingItem),
                     let destinationIndex = sampleItemList.firstIndex(of: sampleItem){
                    withAnimation(.bouncy) {
                      let dropItem = sampleItemList.remove(at: draggingIndex)
                      sampleItemList.insert(dropItem, at:destinationIndex)
                    }
                  }
                }
              }
          }
        })
      }
      .padding(20)
      .navigationTitle("ドラッグ&ドロップ")
      .sheet(item: $selectedItem) { selectedItem in
        Modal(selectedId: selectedItem.id)
      }
    }
  }
}
```

### Transferable
これに準拠させた要素がdrag and dropできるようになる。

```swift: Item
import SwiftUI

struct Item: Codable, Identifiable, Equatable {
  let id: Int
  let title: String
}

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

### draggable(_:preview:)
- 第1引数：ドラッグしているデータ 
Transferableに準拠した型でないとダメ。
- 第2引数preview：ドラッグ中に表示するview
アイコンでも、文字でも表示したいものを設定する。
ちなみにこういうのもトレイリングクロージャ
<details>
  <summary>トレイリングクロージャ</summary>

関数の引数の最後がクロージャの時、クロージャを()の外に書くことができる記法

```swift:サンプル1
Button(action: { print("タップ") }, label: { Text("カウント") })

↓

Button(action: { print("タップ")}) {
 Text("カウント")
}
```

```swift:サンプル2
sample(parameter: 1, handler: { string in
    print(string)
})

↓

sample(parameter: 1) { string in
    print(string)
}
```
</details>

```swift
  .draggable(sampleItem) {
    RoundedRectangle(cornerRadius: 10)
      .fill(.ultraThinMaterial)
      .frame(width: itemSize, height: itemSize)
      .onAppear{
        draggingItem = sampleItem
      }
  }
```
プレビューが現れた時に、ドラッグしている要素をststeに保持する

### dropDestination(for:action:isTargetted)
- 第1引数for：受け取るデータの型
Transferableに準拠した型でないとダメ。
- 第2引数action：ドロップされたデータを処理する
- 第3引数isTargetted：ドロップ領域にアイテムがドラッグされているかどうかをbooleanで返してくれる
こちらの記事ではドラッグ可能になった場合に赤枠を表示している

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

```swift
 .dropDestination(for: Item.self) { items, location in
    draggingItem = nil
    return false
  } isTargeted: { status in
    if let draggingItem, status, draggingItem != sampleItem  {
      if let draggingIndex = sampleItemList.firstIndex(of:draggingItem),
         let destinationIndex = sampleItemList.firstIndex(of: sampleItem){
        withAnimation(.bouncy) {
          let dropItem = sampleItemList.remove(at: draggingIndex)
          sampleItemList.insert(dropItem, at:destinationIndex)
        }
      }
    }
  }
```
ドロップ時に、ドロップ箇所の要素を削除して、ドラッグしてた要素を削除した位置に挿入する。
`if let draggingItem, status, draggingItem != sampleItem`
この条件を間違えるとドロップ時の挙動がおかしくなるので注意。

# 値を渡しながらモーダル遷移

```swift

import SwiftUI

struct ContentView: View {

  @State private var selectedItem: Item? = nil
  ...
  var body: some View {

    NavigationStack {
      GeometryReader {
      ...
              .onTapGesture {
                selectedItem = sampleItem
              }
              ...
      }
      .padding(20)
      .navigationTitle("ドラッグ&ドロップ")
      .sheet(item: $selectedItem) { selectedItem in
        AnswerModal(selectedId: selectedItem.id)
      }
    }
  }
}


struct AnswerModal: View {
  @Environment(\.dismiss) private var dismiss
  var selectedId: Int
  
  var body: some View {
    VStack {
      Text("選択したID: \(selectedId)")
        .font(.title)
        .padding()
      Button("閉じる") {
        dismiss()
      }
    }
    .navigationTitle("モーダル")
    .navigationBarTitleDisplayMode(.inline)
  }
}
```
モーダル遷移に使用する.sheetは2種類あります。
- [**sheet(isPresented:onDismiss:content:)**](https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:))
```isPresented: boolean```で表示非表示
モーダル閉じると自動的にfalseになる

- [**sheet(item:onDismiss:content:)**](https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)) **← 値を渡したい場合はこっち**
```item```がnilじゃなくなるとモーダル表示、nilで非表示
モーダル閉じると自動的にnilになる
itemは```Identifiable```に準拠していないといけない

最初は```sheet(isPresented:onDismiss:content:)```を使ってましたが、1回目の遷移では値がnilになってしまいます。2回目以降は値が渡りました。
けっこう苦しんでから```sheet(item:onDismiss:content:)```の存在を知って解決しました。
これはitemがnilでなくなったら、モーダル表示するのでうまく値が渡りました。
こちらの記事で今回発生した事象と解決方法を説明してくれていました。

https://qiita.com/natmark/items/dbae211fc1819bf51d5b