【SwiftUI】Todoアプリを作ってSwiftUIを理解していく

## はじめに
今回初めてSwiftUIのアプリを作ってみました。
アウトプットとして記事をまとめていきます。

また、作成前に勉強として、[SwiftUI Tutorials](https://developer.apple.com/tutorials/swiftui/)をやってから作成しました。

こうしたほうがいいとか、あと単純に感想とかでもコメントもらえると嬉しいです。

## アプリ概要
諸々書く前にまずは作ったアプリを見てもらいましょう。
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469133/0b0696cb-4dfc-c186-e76e-2e6843c8dc3a.gif)

単純ですが、以下のような画面、機能の構成になります。

- ログイン画面
- Taskの一覧画面
    - ステータスでソートする機能
- Task詳細画面
    - Taskの更新機能
    - Taskの新規登録機能

また、今回は誰かに使ってもらうというよりかは、画面や機能を製造することに重きを置いていて作ったため、DB保存や認証処理等の機能は実装していません。

## 製造で意識した点
### Taskの更新、新規登録で各Viewの情報を更新する
ここは主に[SwiftUI Tutorials](https://developer.apple.com/tutorials/swiftui/)を参考に製造しました。

- モデルの作成
 - Viewを超えて参照するタスク一覧の生成

``` TodoModel.swift
final class TodoModel: ObservableObject {
    @Published var taskList: [Task] = createTaskList()
}
```

- 最上位のエントリポイントで環境変数にセット
    -  @StateObjectを付与し、変数の監視を行う。

```TodoApp.swift
@main
struct TodoApp: App {
    @StateObject private var todoModel = TodoModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(todoModel)
        }
    }
}
```

- TodoListの表示 (モディファイア等の記載は省略)
    - 環境変数から読み込んだListを画面のステータスタブに合わせて表示する。

``` TodoList.swift
struct TodoList: View {
    @EnvironmentObject var todoModel: TodoModel
    @State var selectedTab: TaskStatus
    
    // 画面のステータスタブに合わせてFilter
    var filteredTaskList: [Task] {
        todoModel.taskList.filter {
            $0.status == selectedTab
        }
    }

var body: some View {
        NavigationView {
            VStack{
                List {
                    ForEach(filteredTaskList) { task in
                        NavigationLink(destination: TaskDetail(task: task)){
                            HStack {
                                Text(task.name)                                
                                Text(task.description)
                            }
                        }
                    }
                }

// (中略)

}
```

- Task詳細(登録・更新)画面
    - 環境変数としてtodoModelを読み込む
    - 画面からの入力内容を変数taskに入れる
    - 登録もしくは更新ボタンタップで入力されたtaskの内容を環境変数todoModelに反映させる
        - 登録時→todoModel.taskListを渡し、配列に追加（append)する。
        - 更新時→該当データのIndexを取得して、そのデータを更新する。

```TaskDetail.swift
struct TaskDetail: View {
    @EnvironmentObject var todoModel: TodoModel
    @State var task: Task
    var taskIndex: Int {
        if let taskIndex = todoModel.taskList.firstIndex(where: { $0.id == task.id }) {
            return taskIndex
        } else {
            return todoModel.taskList.count + 1
        }
    }

    var body: some View {
        VStack {
            HStack {
                Spacer()
                
                if taskIndex > todoModel.taskList.count {
                    // 登録
                    RegistTaskButton(todoList: $todoModel.taskList, task: task)
                } else {
                    // 更新
                    UpdateTaskButton(beforeValueTask: $todoModel.taskList[taskIndex], afterValueTask: task)
                }
                
            }
// (中略)
    }
}
```


### TodoListステータスタブのアニメーション
このアニメーションですね。



![ezgif.com-gif-maker (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469133/ecee1ab3-15dc-854d-736c-55b0cccba597.gif)

ここは@hoshi005 さんの記事「[matchedGeometryEffect と @Namespaceを使ったアニメーション](https://qiita.com/hoshi005/items/83cafdc3270bd0e248c0)」を参考に製造しました。

Viewの作りとしては以下の通りです。

- StatusTabButton
    - 各ステータスごとのボタン生成時に汎用的に使用するView

    Preview画像としては以下の画像です。
    ![スクリーンショット 2021-02-17 11.07.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469133/fdbbbc19-9ec2-715a-33f1-72e7301a558f.png)

- TaskStatusTabBar
    - StatusTabButtonをステータスごとに生成し、ステータスバーとしてViewを返却する。

    Preview画像としては以下の画像です。
    ![スクリーンショット 2021-02-17 11.07.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469133/97279844-aaec-e71c-c4ce-a5055ccc8324.png)

#### ボタン側の実装
- 選択されているボタンのステータスを保持（selectedButton）
- このボタンはどのステータスのボタンなのかを保持（selfButtonStatus）
- アニメーション（matchedGeometryEffect）で使用する名前空間ID（namespace）

- 自分のボタンが選択された時(if selectedButton == selfButtonStatus)、matchedGeometryEffectを使用し、ZstackでCapsuleViewをButtonViewに被せる。
    - 引数idは同じアニメーションをさせるボタン間で揃っていれば任意のものでOK

``` StatusTabButton.swift
//（一部モディファイアは省略）
struct StatusTabButton: View {
    
    @Binding var selectedButton: TaskStatus
    let selfButtonStatus: TaskStatus
    var namespace: Namespace.ID

    var title = // selfButtonStatusから設定
    var body: some View {
        
        ZStack {
            if selectedButton == selfButtonStatus {
                Capsule()
                    .fill(Color("TabSelected"))
                    .matchedGeometryEffect(id: "CustomButton", in: namespace)
                
            }
            Button(action: {
                withAnimation{
                    selectedButton = selfButtonStatus
                }
            }){
                Text(title)
                    .foregroundColor(selectedButton == selfButtonStatus ? .white : .gray)
                
            }
        }
    }
}
```

#### ステータスバー側の実装
- 選択されているボタンのステータスを保持（selectedButton）
- @Namespaceプロパティラッパーを使用して変数を定義(namespace)
- あとはStatusTabButtonを生成しているだけ

```TaskStatusTabBar.swift
//（一部モディファイアは省略）
struct TaskStatusTabBar: View {
    @Binding var selectedTab: TaskStatus
    @Namespace var namespace
    
    var body: some View {
        HStack {
            ForEach(TaskStatus.allCases, id: \.self) { status in
                StatusTabButton(selectedButton: $selectedTab, selfButtonStatus: status, namespace: namespace)
            }
        }
    }
}
```

### 更新画面と新規登録画面を同一Viewで作成
Viewの汎用性をあげるため、今回は更新画面と新規登録画面を同一のViewで作成しました。
[SwiftUI Tutorials](https://developer.apple.com/tutorials/swiftui/)のFavorite機能を参考に、登録・更新ボタンのみViewを分け、他は同一のViewで実装しました。
ソースは上記の「TaskDetail.swift」のソースを参照してください。

各ButtonViewの生成はtaskIndexがヒットしたかどうかで、更新用のView（UpdateTaskButton）か、登録用のView（RegistTaskButton）かで使い分けてます。


## 最後に
一覧画面でステータスを変えた時に、Listの挙動が若干変なアニメーションっぽくなるのなんとかならないですかね。。。

全体のソースはここにあります。
https://github.com/skyTiki/TodoAPP

## 参考
[SwiftUI Tutorials](https://developer.apple.com/tutorials/swiftui/)
[[Swift] SwiftUIのチートシート](https://qiita.com/hcrane/items/eb847ca7fb7a0b9e8073)
[【SwiftUI】シートの使い方（sheet）](https://capibara1969.com/2521/)
[matchedGeometryEffect と @Namespaceを使ったアニメーション](https://qiita.com/hoshi005/items/83cafdc3270bd0e248c0)
[Dating Login With Firebase](https://kavsoft.dev/SwiftUI_2.0/DLogin/)
[【第1回】日本語版SwiftUIチュートリアル【基本要素を学ぶ】](https://hirlab.net/nblog/category/programming/art_1375/)