【SwiftUI】シンプルにドラッグで並び替えできる List を 作りたい 

# 🤔 動機

SwiftUIのデフォルトのアニメーションやトランジションは、視覚的に自然で滑らかなユーザー体験を提供します。特に、Listビューや配列の並び替えに関しては、自動的に適用されるアニメーションが便利です。

SwiftUIのデフォルトアニメーションとトランジションは、少ないコードで視覚的にリッチな動作を提供し、特にリストビューの並び替えや要素の追加/削除において自然な体験をもたらします。

# 🤔 実装

List は使いたくありません。

https://developer.apple.com/documentation/swiftui/list

まず、デザインの自由度の低さです。Listは標準的な機能を提供する一方で、カスタマイズ性が制限されており、独自のUIを作りたい場合には不便です。たとえば、セルごとの高度なレイアウトや独自のタッチジェスチャーを実装するのが難しく、UIの細部にこだわりたい場面ではListが適していないと感じることがあります。

次に、パフォーマンスの問題があります。Listは小規模なデータセットであれば問題ありませんが、大量のデータを扱う際にスクロール性能が低下したり、リストの再レンダリング時にカクついたりすることがあります。特に動的なリストや頻繁に更新が必要なアプリでは、このパフォーマンスの低下がユーザー体験に悪影響を与えるため、避けられがちです。

また、アニメーションやトランジションが思い通りに動かないこともあります。Listではデフォルトのアニメーションが適用されますが、カスタムアニメーションやトランジションを適用しづらく、見栄えの良いUI作成に制限がかかることがあります。

ということで ScrollView, LazyVStack, ForEach 入れ子で作ります。

```swift
struct Fruit: Identifiable, Equatable {
  let id: Int
  let name: String
  let color: Color
}
```
```swift
struct FruitItemView: View {
  var fruit: Fruit

  var body: some View {
    HStack {
      Spacer()
      Text("\(fruit.name)")
      Spacer()
    }
    .foregroundStyle(.background)
    .padding(.vertical, 40)
    .background(fruit.color)
    .cornerRadius(16)
  }
}
```

```swift
struct DraggableList: View {

  @State private var fruits: [Fruit] = [
    Fruit(id: 1, name: "APPLE", color: .red),
    Fruit(id: 2, name: "ORANGE", color: .orange),
    Fruit(id: 3, name: "BANANA", color: .yellow),
    Fruit(id: 4, name: "MELON", color: .green),
    Fruit(id: 5, name: "PEACH", color: .pink),
    Fruit(id: 6, name: "KIWI", color: .brown),
    Fruit(id: 7, name: "GRAPES", color: .purple),
    Fruit(id: 8, name: "LIME", color: .cyan),
    Fruit(id: 9, name: "TOMATO", color: .indigo)
  ]

  @State private var dragging: Fruit?

  var body: some View {
    ScrollView {
      LazyVStack(spacing: 16) {
        ForEach(fruits) { fruit in
          FruitItemView(fruit: fruit)
        }
      }
    }
    .padding()
  }
}
```

![sc 2024-09-09 at 23.43.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588914/7adfcd95-e381-4b57-600c-24321f4f1d86.png)

ここからスタートします。

# 🤔 シャッフルしてみる

withAnimation を使ってシャッフルしてみます。

```swift
    ScrollView {
      LazyVStack(spacing: 16) {
        ForEach(fruits) { fruit in
          FruitItemView(fruit: fruit)
        }
      }
    }
    .padding()
    .task {
      while true {
        try? await Task.sleep(for: .seconds(1))
        withAnimation {
          fruits = fruits.shuffled()
        }
      }
    }
```

![sc 2024-09-10 at 21.50.50.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588914/681a30b4-5b97-ffc5-3f72-5cb6c0a69348.gif)

配列の並び替えに関しては、使えそうなアニメーションです。

# 🤔 スワップする

```swift
    ScrollView {
      LazyVStack(spacing: 16) {
        ForEach(fruits) { fruit in
          FruitItemView(fruit: fruit)
        }
      }
    }
    .padding()
    .task {
      while true {
        try? await Task.sleep(for: .seconds(1))
        withAnimation {
          let from = Int.random(in: 0 ..< fruits.count)
          let to = Int.random(in: 0 ..< fruits.count)
          fruits.swapAt(from, to)
        }
      }
    }
```

![sc 2024-09-10 at 22.09.45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588914/3c02a6bb-7053-b90a-47f5-260e5f63b5e7.gif)

ということで、アニメーションは withAnimation でまあまあいけそうです。

# 🤔 ドラッグ&ドロップする

onDrag と onDrop と DropDelegate を使うとよさげです。

onDrag、onDrop、およびDropDelegateを使うと、SwiftUIでリストや要素の並び替えやドラッグ＆ドロップ操作をカスタマイズでき、Listの制約を回避できるため、非常に有効です。その理由は以下の通りです。

### 1. カスタマイズ性の向上
onDragやonDropを利用すると、リストの要素をユーザーのドラッグ操作に応じて動的に移動させたり、データをドロップした際にどのように処理するかを細かく制御できます。これにより、Listの制限されたレイアウトや操作性を超えた、自由なUIを構築することが可能です。

### 2. 柔軟な並び替え機能
DropDelegateを活用することで、ドラッグされた要素のドロップ先や並び替えの処理を柔軟に制御できます。これにより、単純なリストだけでなく、複雑なデータ構造やカスタムビュー内での要素の並び替えを実装できます。これらの操作はListでは難しいですが、onDragとonDropを使えば、より直感的な操作が実現できます。

### 3. ユーザー体験の向上
ドラッグ＆ドロップは、多くのユーザーにとって直感的で使いやすい操作です。onDragとonDropを使うことで、要素の移動や並び替えをシームレスに行えるため、ユーザー体験が大幅に向上します。特に、視覚的なフィードバックやカスタムアニメーションを追加することで、よりインタラクティブで魅力的なアプリを作成できます。

### 4. Listの制約回避
Listでは標準的なアニメーションやレイアウトの制約があり、カスタムな操作や見た目の実装が難しい場面がありますが、onDragやonDrop、DropDelegateを組み合わせることで、その制約を回避し、より多様なインターフェースを作成できます。


# 🤔 まとめ

以下のようになりました。

![sc 2024-09-12 at 19.20.55.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588914/a3a78487-e80f-4b0c-d4ed-763badcc2c77.gif)

# 🤔 参考

ソースコード、詳細説明はこちらから。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">【SwiftUI】Create Draggable Reorder ListView without List <a href="https://twitter.com/hashtag/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0?src=hash&amp;ref_src=twsrc%5Etfw">#プログラミング</a> <a href="https://twitter.com/hashtag/%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2?src=hash&amp;ref_src=twsrc%5Etfw">#エンジニア</a><a href="https://t.co/4CYemmy7oF">https://t.co/4CYemmy7oF</a></p>&mdash; chanzmao (@maochanz) <a href="https://twitter.com/maochanz/status/1834392802882474466?ref_src=twsrc%5Etfw">September 13, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>