SwiftUIでtodoアプリをつくってみた

## はじめに

[アイスタイル Advent Calendar 2023](https://qiita.com/advent-calendar/2023/istyle)の13日目を担当させて頂きます、こたちゃんです:heart_eyes:
私は、iOSのアプリチームで新卒のエンジニア2年目になります👩‍💻
今回はSwiftUI[^1]でTODOアプリを作ってみました😌

[^1]: SwiftUIとは、iPhoneだけでなくiPadやMacOSX、Apple Watchなど、Apple製品のプラットフォームすべてに対応しているUIフレームワークです。

## 作成するアプリ

<img width="380" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/dcdee953-f73a-2b37-008a-b2cc74993765.gif">

## 仕様・要件

- ToDoリスト画面
  - 入力できるTextFieldがある
  - 「追加」というボタンがある
  - ボタンをクリックすると入力したテキストがリストに反映される
  - テキストがなにも入力されていない時は送信されないようにする
  - リストにはタスクとチェックボックスが表示される
  - チェックボックスはタップするとチェックの切り替えができる
  - チェックボックスをtrueにすると打ち消し線も表示される

## このアプリ開発で学べること
- Swiftの基礎
  - struct
  - indices
  - ForEach
  - 三項演算子
  - toggle
- SwiftUIの基礎
  - Stack
  - Text
  - TextField
  - Button
  - ＠Status


## 実装手順
1. アプリの雛形作成
2. ContentView画面を修正する
3. TodoListView画面を修正する
4. toDoItemViewに切り出す
5. TextFieldの追加
6. チェックボックスをタップしたらボタンが切り替わる
7. タスクの新規作成

## 1. アプリの雛形作成

 Xcodeを立ち上げると「Xcode」と表示されており、その下に3つの項目があります。
※ 今回はXcode15を使用しております。

新しいiOSアプリのプロジェクトを作成するため、下記を選択してください。

![スクリーンショット 2023-11-30 15.39.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/eb052e55-4d40-d1fd-a216-70ed8cf5994b.png)

iOSのAppを選択して「Next」ボタンをクリックください。

![スクリーンショット 2022-12-15 19.03.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/e2d00d19-20a0-6ddf-fb75-208cb94afd3a.png)

次にProductNameに「Todo」と入力します。
Interfaceが`SwiftUI`か確認した後に「Next」ボタンをクリックしてください。

![qq.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/d4631d97-021e-11e0-a588-c2fbde25f56a.png)

アプリファイルの保存は、ご自身のPCの好きな場所を選択し「Create」ボタンをクリックしてください。
下記のような画面になりましたら雛形作成が完了です。

![スクリーンショット 2023-12-04 16.13.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/b7b75551-286b-8ab1-3e12-0bf31e9ab929.png)

さっそく、実装を始めていきます！！

## 2. ContentView画面を修正する

まずは左側の並んでいるファイルツリーの中から**TodoApp**を選択して指を２つ添えて右クリックして「New File...」をクリックします。

![スクリーンショット 2023-12-04 15.34.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/46695833-730b-2e34-20b0-229653985be5.png)

次にiOSのSwift Fileを選択し「Next」ボタンをクリックします。

![スクリーンショット 2023-11-30 16.01.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/81f3795f-959b-b631-7087-beee199f2fe3.png)

ファイル名は`TodoListView.swift`と入力して「Create」ボタンをクリックします。

![スクリーンショット 2023-12-04 15.37.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/63c743c9-ca3c-a243-8e95-99d93b8eccfc.png)

作成した`TodoListView`を表示するために
`ContentView.swift`を開き、下記のように修正します。

```diff_swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
-           Image(systemName: "globe")
-               .imageScale(.large)
-               .foregroundStyle(.tint)
-           Text("Hello, world!")
+           TodoListView()
        }
-       .padding()
    }
}

#Preview {
    ContentView()
}
```

## 3. TodoListView画面でリスト部分を作成しよう

それでは`TodoListView`に実装していきます。
ToDoリストを作成する上で必要なデータは`タスクの内容`と`完了したかどうかの有無`です。
`タスクの内容`は**task**としてString型
`完了したかどうかの情報`は**isChecked**としてBool型で持ちます。

まずは下記のように仮の配列データを作成します。

```diff_swift:TodoListView.swift
import SwiftUI

struct TodoListView: View {
+   var todoLists: [(isChecked: Bool, task: String)] = [
+       (isChecked: false, task: "読書する"),
+       (isChecked: true, task: "掃除する"),
+       (isChecked: false, task: "散歩する")
+   ]
    
    var body: some View {
    
    }
:
:
```

続いて、作成したデータを表示するために`ForEach`を使用していきます。

```diff_swift:TodoListView.swift
import SwiftUI

struct TodoListView: View {
    var todoLists: [(isChecked: Bool, task: String)] = [
        (isChecked: false, task: "読書する"),
        (isChecked: true, task: "掃除する"),
        (isChecked: false, task: "散歩する")
    ]
    
    var body: some View {
+       VStack {
+           Text("ToDoList")
+               .font(.system(size: 30, weight: .bold, design: .default))
+           ForEach(todoLists.indices, id: \.self) { index in
+               HStack {
+                   Image(systemName:
+                       todoLists[index].isChecked ? "checkmark.square" : "square"
+                   )
+                   Text(todoLists[index].task)
+               }
+               .padding(.top, 1)
+               .padding(.leading, 20)
+               .frame(maxWidth: .infinity, alignment: .leading)
+           }
+       }

+       Spacer()
    }
    
:
:
```

:::note warn
レイアウトを整えるためのコード説明(padding,frameなど)は省略しています。
気になる方はぜひ調べてみてください。
:::

細かく説明していきます。

### VStack
一番大きな枠が`VStack`で囲まれていますがこれは**垂直方向**に並んで配置されます。
つまり、要素の下に配置されていくようなイメージです。


### Text
Textの()内に文字列や数字を指定すると画面に表示されます。
文字列で "ToDoList"と指定していますがこれはタイトルに当たります。
```swift
Text("ToDoList")
```

### ForEach
ForEach文は繰り返し処理の1つです。
ForEach文ではbreakやreturnなどで処理を抜けることができません。

`todoLists.indices`ですが「配列名.indices」とすると、
クロージャ[^2]の引数として配列の**index**を取得できます。
[^2]: クロージャとは、変数をスコープ内に閉じ込めるためのデータ構造のことです。
`array[index]` という形式のため今回ですと`todoLists[index]`となります。
idに.selfを渡していますが、
これはForEachの中身を一意に識別するためのIDとして、それぞれの数値自体を使用しています。
配列の一意性を保証したデータコレクションとして渡せます。
```swift
ForEach(todoLists.indices, id: \.self) { index in
    
}
```

### HStack ・ 三項演算子
今回は２つの要素を**横に並べて**表示したいため`HStack`で囲んでいます。

そしてImage()ではタスクの完了の有無によって画像を出し分けたいため三項演算子を使用しています。
isCheckedが`ture`の場合 → "checkmark.square"
isCheckedが`false`の場合 → "square" が表示されるように実装しました。

Text()には`todoLists[index].task`を表示するように指定しています。
そのため、今の段階ではtodoListsに指定してある3つの要素が表示されています。

```swift
HStack {
   Image(systemName:
       todoLists[index].isChecked ? "checkmark.square" : "square"
   )
   Text(todoLists[index].task)
}
```

表示するImageのアイコンは用意されているものを使用します。
下記の赤色で囲んである「＋」ボタンをクリックします。

![スクリーンショット 2023-12-04 17.08.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/3f3d96bf-5202-1a94-a255-997e58156590.png)

一番右側にある星マークのアイコンをクリックしてみるとたくさんのアイコンが表示されます。
この中から使用したいアイコンを選び、ドラッグ&ドロップで`TodoListView.swift`においてください。
![スクリーンショット 2023-11-30 16.44.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/7b06f303-2a92-652c-8c69-dad79f218c89.png)

私は「checkmark」と検索して以下の画像を使用してます。

![スクリーンショット 2023-12-04 18.33.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/3d8c6d10-a55b-3aa2-d1fd-529c575e6981.png)

ここまででシミュレーターを確認してみると以下のようになっていると正解です。

![スクリーンショット 2023-12-04 17.08.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/366097bf-484e-3ca2-146e-65c3341ac822.png)


## 4. TextFieldの追加

続いて、TextFieldを追加してタスクの新規追加ができるようにします。
まずは全体のコードが以下になります。

```diff_swift:TodoListView.swift
struct TodoListView: View {
+   @State var newTask: String = ""
    var todoLists: [(isChecked: Bool, task: String)] = [
        (isChecked: false, task: "読書する"),
        (isChecked: true, task: "掃除する"),
        (isChecked: false, task: "散歩する")
    ]
    
    var body: some View {
        VStack {
            Text("ToDoList")
                .font(.system(size: 30, weight: .bold, design: .default))
+           HStack {
+               TextField("タスクを入力してください", text: $newTask)
+                   .textFieldStyle(.roundedBorder)
+                   .padding(EdgeInsets(
+                       top: 10,
+                       leading: 20,
+                       bottom: 10,
+                       trailing: 15
+                   ))
+               Button("追加", action: {})
+                   .padding(.trailing, 20)
+           }
            ForEach(todoLists.indices, id: \.self) { index in
                HStack {
                    Image(systemName:
                            todoLists[index].isChecked ? "checkmark.square" : "square"
                    )
                    Text(todoLists[index].task)
                }
                .padding(.top, 1)
                .padding(.leading, 20)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        
        Spacer()
    }
}
```

### ＠State

＠Stateは**値の更新を監視**し、通知が来たら自動でViewを**再描画**します。
SwiftUIのViewではstruct内に保持するプロパティを変更できませんが
＠Stateを使用するとstruct内のプロパティを変更可能にしてくれます。
下記でいうプロパティは、newTaskを指しています。

＠Stateで管理するため、TextFieldで入力した内容に変更が可能になっています。

```swift
@State var newTask: String = ""
```

### TextField

TextFieldの`第一引数`には**プレースホルダ**として表示されるテキストを設定します。
今回は「タスクを入力してください」としています。

`第二引数`には変数を指定する際に、頭文字に「$」を付与しています。
「$」をつけることで、その変数の「参照」を渡します。
変数とTextFieldのデータを双方向に紐付けているというイメージです。
このように、「text」に状態変数のプロパティを紐づけることでTextFieldを使用できます。

```swift
TextField("タスクを入力してください", text: $newTask)
```

### Button

Buttonの`第一引数`にはボタンとして表示される文字列を指定できます。
`第二引数`には、actionを指定できます。
ボタンをクリックした際に行われてほしい処理を記述します。
actionは一旦、`{}`にしておきます。

```swift
Button("追加", action: {})
```

ここまでで下記のようになっていれば正解です。
![スクリーンショット 2023-12-04 18.57.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2098611/df0d5439-c84a-bf72-7005-5437dc8e1d78.png)


## 5. ToDoItemの追加

現在は`[(isChecked: Bool, task: String)]`と記述していますが
structに切り出してToDoItemとして使用します。
`struct`[^3]は構造体といいます。簡単にまとめると継承のできないクラスのようなものです。
[^3]: structは値型に分類され、値の受け渡しが「変更を共有しないもの」となっています。
データをオブジェクトの内部に隠して、容易に値を変更できなくするための入れ物のようなイメージです。
これで型の指定には`[ToDoItem]`と記述できるようになります。

```diff_swift:TodoListView.swift
import SwiftUI

+ struct ToDoItem {
+     var isChecked: Bool
+     var task: String
+ }

struct TodoListView: View {
    @State var newTask: String = ""
-   var todoLists: [(isChecked: Bool, task: String)] = [
-       (isChecked: false, task: "読書する"),
-       (isChecked: true, task: "掃除する"),
-       (isChecked: false, task: "散歩する")
+   var todoLists: [ToDoItem] = [
+       ToDoItem(isChecked: false, task: "読書する"),
+       ToDoItem(isChecked: true, task: "掃除する"),
+       ToDoItem(isChecked: false, task: "散歩する")
    ]
    
    var body: some View {
        VStack {
            Text("ToDoLis
```

## 6. チェックボックスをタップしたらボタンが切り替わる

そして次にチェックボックスをタップしたらチェックが切り替わるようにしていきます。

```diff_swift:TodoListView.swift
struct TodoListView: View {
    @State var newItem: String = ""
    var todoLists: [ToDoItem] = [
        ToDo(isChecked: false, task: "読書する"),
        ToDo(isChecked: true, task: "掃除する"),
        ToDo(isChecked: false, task: "散歩する")
    ]

    var body: some View {
        VStack {
        
            ... 省略
            
            ForEach(todoLists.indices, id: \.self) { index in
                HStack {
+                   Button(action: {
+                       todoLists[index].isChecked.toggle()
+                   }, label: {
                        Image(systemName:
                                todoLists[index].isChecked ? "checkmark.square" : "square"
                        )
                        .imageScale(.large)
                        .foregroundStyle(.pink)
+                   })
                    Text(todoLists[index].task)
                }
                .padding(.top, 1)
                .padding(.leading, 20)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        
        Spacer()
    }
}
```

Button()のlabelにはタップする要素を記述します。
今回はチェックボックスを画像で表示しているのでlabel内にImage()を置きます。

### toggle

bool型の真偽を入れ替えたい変数名の後に、.toggle()を付けます。
そのため`.toggle()`によって画像が切り替わるようになっています。


ただ、このままでは下記のエラーが出てしまいます。
```
Cannot use mutating member on immutable value: 'self' is immutable
```

SwiftUIのViewではstruct内に保持するプロパティを変更できないため
todoListsは、`＠State`をつける必要があります。

```diff_swift:TodoListView.swift
struct TodoListView: View {
    @State var newItem: String = ""
-   var todoLists: [ToDoItem] = [
+   @State var todoLists: [ToDoItem] = [
        ToDo(isChecked: false, task: "読書する"),
        ToDo(isChecked: true, task: "掃除する"),
        ToDo(isChecked: false, task: "散歩する")
    ]
```

## 7. タスクの新規作成

TextFieldに入力した内容をリストに表示させます。

```diff_swift:TodoListView.swift
:
:
struct TodoListView: View {
    @State var newTask: String = ""
    @State var todoLists: [ToDoItem] = []
    
    var body: some View {
        VStack {
            Text("ToDoList")
                .font(.system(size: 30, weight: .bold, design: .default))
            HStack {
                TextField("タスクを入力してください", text: $newTask)
                    .textFieldStyle(.roundedBorder)
                    .padding(EdgeInsets(
                        top: 10,
                        leading: 20,
                        bottom: 10,
                        trailing: 15
                    ))
                Button("追加", action: {
+                   todoLists.append(
+                       ToDoItem(isChecked: false, task: newTask)
+                   )
+                   newTask = ""
                })
                .padding(.trailing, 20)
            }
:
:
```

### append
appendメソッドは配列に要素を追加する処理です。
`newTask`を`task`に追加することでタスクを新規作成しています。
また、タスクを追加した際にTextField内は空にしたいので "" を代入します。

```swift
Button("追加", action: {
    todoLists.append(
        ToDoItem(isChecked: false, task: newTask)
    )
    newTask = ""
})
.padding(.trailing, 20)
.disabled(newTask.isEmpty)
```

これでTodoリストは完成です！

## 挑戦

<details><summary>TextFieldが空白の時に追加ボタンをクリックしても新規作成されないようにする</summary><div>

disabled()とisEmptyを使用しています。
disabledはボタンを押せなくするという役割があり、`true`の場合はボタンが押せなくなります。
isEmptyは空白かどうかのチェックをしてくれるため
空白の場合は`true`が返されボタンが押せません。
空白でない場合は`false`が返され新規作成する処理を実行しています。
```swift:TodoListView.swift
Button("追加", action: {
    todoLists.append(
        ToDoItem(isChecked: false, task: newTask)
    )
    newTask = ""
})
.padding(.trailing, 20)
.disabled(newTask.isEmpty)
```

</div></details>

<details><summary>isCheckedがtrueの時、テキストに打ち消し線をいれる</summary><div>

strikethrough()で打ち消し線をいれることができます。
if文でisCheckedがtrueの時、表示されるようにしています。
```swift:TodoListView.swift
if todoLists[index].isChecked {
    Text(todoLists[index].task)
        .strikethrough()
} else {
    Text(todoLists[index].task)
}
```

</div></details>

## 完成コード

```swift:TodoListView.swift
import SwiftUI

struct ToDoItem {
    var isChecked: Bool
    var task: String
}

struct TodoListView: View {
    @State var newTask: String = ""
    @State var todoLists: [ToDoItem] = []
    
    var body: some View {
        VStack {
            Text("ToDoList")
                .font(.system(size: 30, weight: .bold, design: .default))
            HStack {
                TextField("タスクを入力してください", text: $newTask)
                    .textFieldStyle(.roundedBorder)
                    .padding(EdgeInsets(
                        top: 10,
                        leading: 20,
                        bottom: 10,
                        trailing: 15
                    ))
                Button("追加", action: {
                    if !newTask.isEmpty {
                        todoLists.append(
                            ToDoItem(isChecked: false, task: newTask)
                        )
                        newTask = ""
                    }
                })
                .padding(.trailing, 20)
            }
            ForEach(todoLists.indices, id: \.self) { index in
                HStack {
                    Button(action: {
                        todoLists[index].isChecked.toggle()
                    }, label: {
                        Image(systemName:
                                todoLists[index].isChecked ? "checkmark.square" : "square"
                        )
                        .imageScale(.large)
                        .foregroundStyle(.pink)
                    })
                    if todoLists[index].isChecked {
                        Text(todoLists[index].task)
                            .strikethrough()
                    } else {
                        Text(todoLists[index].task)
                    }
                }
                .padding(.top, 1)
                .padding(.leading, 20)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            
            Spacer()
        }
    }
}

#Preview {
    TodoListView()
}

```

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            TodoListView()
        }
    }
}

#Preview {
    ContentView()
}
```

## 最後に

今年もSwiftでの記事を書かせていただきました。
SwiftUIは、まだまだ記事も少ないためぜひ参考にしていただけるとうれしいです！❤️
素敵なクリスマスを:santa_tone2::snowflake:

## 参考文献

https://qiita.com/uhooi/items/180068e9b27e4b85fc9e

https://qiita.com/Ren_yello/items/df88dee9adb401b6335d

https://tokizuoh.hatenablog.com/entry/2022/06/24/002913

https://capibara1969.com/3825/

https://i-app-tec.com/swiftui/vstack-hstack.html