SwiftUIでのページ遷移デザイン

SwiftUIで開発の初めにページ遷移の土台を作る機会があったのでこんな感じに作ったので載せて起きます。
作り方の好みもありますので参考程度で。
環境はiOS16で作成しています。


## デモ画像

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35973/c078d60f-3a2a-44b5-5331-8aeb98d5cfaf.gif)


## 特定のViewを開いて開発する

Previewで実装と絡めた開発をしていくのにも限界があります。
特に毎回画面遷移して作業するとトライ＆エラーの効率が悪く、特定のViewを開いて開発をしたいことが多く出てきます。
そういった事情から特定のViewを指定してシュミレータで作業できるように作りました。
コードの上部分の`destination`で指定して開きます。

```Swift
var destination: Destination? = nil
//var destination: Destination? = .TodoListView
//var destination: Destination? = .TodoDetailView(Todo(id: 5, value: "hoge"))
```

### 画面遷移するViewを定義する

手間ではありますが画面遷移で利用するViewを継ぎ足してやる必要があります。
enumの定義に加えて、`SelectDistinationView`の分岐を増やす構造としました。

```Swift
enum Destination: Hashable {
  case IndexView
  case TodoListView
  case TodoDetailView(Todo)
}

@main
struct RoutingExampleApp: App {
  @ObservedObject var param = Param()
  
  var body: some Scene {
    WindowGroup {
      SelectView()
        .environmentObject(param)
    }
  }
  
  func SelectView() -> AnyView {
    return AnyView (
      NavigationStack(path: $param.path) {
        SelectDistinationView(destination)
        .navigationDestination(for: Destination.self) { value in
          LazyView(SelectDistinationView(value))
        }
      }
    )
  }
  func SelectDistinationView(_ destination: Destination?) -> AnyView {
    return AnyView(
      Group {
        switch destination {
        case .IndexView: IndexView()
        case .TodoListView: TodoListView()
        case .TodoDetailView(let todo): TodoDetailView(todo: todo)
        default:
          LazyView(IndexView())
        }
      }
    )
  }
}
```

## ページの引数をどう扱うか

大体の引数はObservableObjectで引き回して使いました。
セッションのように一時的なUser情報等を放り込んだりするのに便利です。
ここではページ情報(`Destination`)の引数だけ設定しています。

```Swift
class Param: ObservableObject {
  @Published var path = [Destination]()
}
```

IndexViewでは`EnvironmentObject`で`param`を取得して、`param.path.append(.TodoListView)`で画面遷移としています。

```Swift
struct IndexView: View {
  @EnvironmentObject var param: Param
  
  var body: some View {
    VStack {
      Button {
        param.path.append(.TodoListView)
      } label: {
        Text("TodoList")
      }
      .buttonStyle(.borderedProminent)
    }
    .padding()
  }
}
```

## リストから詳細に値を渡す

次に、ToDoListの場合は詳細ページに引数を渡す必要があります。
試したところ`NSObject`のclassであれば簡単に引き回せました。Realmでも`Object`(中身はNSObject)であれば問題ないようです。
ここでは`param.path.append`ではなく、`NavigationLink`を使用し、enumに指定した`Destination`に値を渡して遷移しています。
挙動としては`append`するのと何ら変わりません。

```Swift
final class Todo: NSObject, Identifiable {
  var id: Int
  var value = ""
  var date = Date()
  
  init(id: Int, value: String) {
    self.id = id
    self.value = value
  }
}

struct TodoListView: View {
  @EnvironmentObject var param: Param
  var todoList = [
    Todo(id: 1, value: "Hello"),
    Todo(id: 2, value: "Hello"),
    Todo(id: 3, value: "Hello"),
    Todo(id: 4, value: "Hello"),
    Todo(id: 5, value: "Hello"),
  ]
  var body: some View {
    VStack {
      List {
        ForEach(todoList){ todo in
          NavigationLink(value: Destination.TodoDetailView(todo)){
            Text("\(todo.value) \(todo.id)" )
          }
        }
      }
    }
    .padding()
  }
}

struct TodoDetailView: View {
  @Environment(\.dismiss) var dismiss
  var todo: Todo
  var body: some View {
    VStack {
      Text("\(todo.value) \(todo.id)" )
    }
    .toolbar {}
  }
}
```

## ルートページへの戻り方

ちなみにですが、ルートページに戻る際は、`param.path.removeLast(param.path.count)`で戻れます。

```Swift
struct TodoDetailView: View {
  @EnvironmentObject var param: Param
  var todo: Todo
  var body: some View {
    VStack {
      Text("\(todo.value) \(todo.id)" )
    }
    .toolbar {
      ToolbarItem(placement: .bottomBar) {
        Button(action: {
          param.path.removeLast(param.path.count)
        }) {
          Text("最初に戻る")
            .fontWeight(.regular).font(.title)
        }
        .buttonStyle(.borderedProminent)
      }
    }
  }
}
```

## おわりに

いかがだったでしょうか？
もっと自分ならいい感じに作れるといった意見もあったかと思います。
役に立った方は👍いいねを頂ければ幸いです。


## サンプルコード

https://gist.github.com/mbotsu/161fe5d82b3d9f61562193af3f8ab02c