【初めてのiOSアプリ開発】SwiftUIでTodoアプリ作ってみた（JSONデータの永続化含む）

今回は、完全初心者向けなので、MVVMモデルには従っていません。
また、Xcodeの設定に関しても説明を省いているので、そこはクリアしてから読み進めてください。

ステップごとにコードの解説をしていますが、「ふ〜ん、なるほど、わからん」くらいで流しちゃって大丈夫です。まずはコードを書いて、実際に問題なくアプリが動いたら、そこから、ChatGPTやClaudeでコードや用語の解説をしてもらってください。

## ステップ１：　タスクのデータモデルを定義
`⌘ + N` -> SwiftUI View -> UserTask.swift(ファイル名はお好きにどうぞ)
```swift
import SwiftUI

struct Task: Identifiable, Codable, Equatable {
    let id: UUID
    var title: String
    var checked: Bool
    
    init(id: UUID = UUID(), title: String, checked: Bool) {
        self.id = id
        self.title = title
        self.checked = checked
    }
}
```
#### コード解説
1. `struct Task: Identifiable, Codable, Equatable`
   - `Task` という構造体を定義しています。
   - `Identifiable` プロトコルに準拠することで、各タスクに一意の識別子を持たせます。これは SwiftUI の List や ForEach で使用する際に便利です。
   - `Codable` プロトコルに準拠することで、この構造体を JSON などにエンコード/デコードできるようになります。これはデータの保存や読み込みに使用されます。
   - `Equatable` プロトコルに準拠することで、2つの Task インスタンスが等しいかどうかを比較できるようになります。

2. プロパティ
   - `let id: UUID`: 各タスクの一意の識別子です。`let` で宣言されているため、一度設定されると変更できません。
   - `var title: String`: タスクのタイトルを保持します。`var` で宣言されているため、後から変更可能です。
   - `var checked: Bool`: タスクが完了したかどうかを示すフラグです。これも `var` で、変更可能です。

3. イニシャライザ
   ```swift
   init(id: UUID = UUID(), title: String, checked: Bool) {
       self.id = id
       self.title = title
       self.checked = checked
   }
   ```
   - このイニシャライザは新しい Task インスタンスを作成するために使用されます。
   - `id` パラメータにはデフォルト値 `UUID()` が設定されています。これにより、id を指定せずに新しい Task を作成すると、自動的に新しい UUID が生成されます。
   - `title` と `checked` は必須パラメータで、Task 作成時に必ず指定する必要があります。

この `Task` 構造体は、アプリケーション全体でタスクを表現するために使用されます。各タスクは一意の ID、タイトル、完了状態を持ち、簡単に識別、保存、比較することができます。


## ステップ２：　タスクデータの管理と永続化
新規ファイル（UserData.swift)作成。作成方法はステップ１と同じ。
```swift
import SwiftUI

class UserData: ObservableObject {
    @Published var tasks: [Task] = []
    @Published var isEditing: Bool = false
    @Published var draftTitle: String = ""
    
    init() {
        loadTasks()
    }
    
    func resetDraft() {
        draftTitle = ""
        isEditing = false
    }
    
    func addTask(_ task: Task) {
        tasks.insert(task, at: 0)
        saveTasks()
    }
    
    func saveTasks() {
        do {
            let data = try JSONEncoder().encode(tasks)
            try data.write(to: getDocumentsDirectory().appendingPathComponent("tasks.json"))
        } catch {
            print("Failed to save tasks: \(error.localizedDescription)")
        }
    }
    
    func loadTasks() {
        do {
            let fileURL = getDocumentsDirectory().appendingPathComponent("tasks.json")
            let data = try Data(contentsOf: fileURL)
            tasks = try JSONDecoder().decode([Task].self, from: data)
        } catch {
            print("Failed to load tasks: \(error.localizedDescription)")
            tasks = []
        }
    }
    
    private func getDocumentsDirectory() -> URL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }
}
```
#### コード解説
1. クラス定義：
   ```swift
   class UserData: ObservableObject
   ```
   - `ObservableObject`プロトコルに準拠することで、このクラスのインスタンスの変更をSwiftUIビューが監視できるようになります。

2. プロパティ：
   ```swift
   @Published var tasks: [Task] = []
   @Published var isEditing: Bool = false
   @Published var draftTitle: String = ""
   ```
   - `@Published`属性は、プロパティの変更をObservableObjectが自動的に発行するようにします。
   - `tasks`はタスクのリストを保持します。
   - `isEditing`は新しいタスクの追加モードかどうかを示します。
   - `draftTitle`は新しいタスクのタイトルの下書きを保持します。

3. 初期化：
   ```swift
   init() {
       loadTasks()
   }
   ```
   - インスタンス作成時に`loadTasks()`を呼び出し、保存されているタスクを読み込みます。

4. メソッド：
   - `resetDraft()`: 新しいタスクの入力をリセットします。
   - `addTask(_:)`: 新しいタスクを追加し、保存します。
   - `saveTasks()`: タスクをJSONとしてファイルに保存します。
   - `loadTasks()`: ファイルからタスクを読み込みます。
   - `getDocumentsDirectory()`: アプリのドキュメントディレクトリのURLを取得します。

5. エラーハンドリング：
   - `saveTasks()`と`loadTasks()`メソッドでは、`do-catch`文を使用してエラーを処理しています。

6. データの永続化：
   - `JSONEncoder`と`JSONDecoder`を使用して、タスクデータをJSONフォーマットで保存・読み込みします。
   - ファイルの保存・読み込みには`FileManager`を使用しています。

このクラスは、アプリケーションのデータ管理の中心的な役割を果たしています。タスクの追加、保存、読み込みなどの操作をカプセル化し、SwiftUIのビューから簡単にアクセスできるようにしています。また、`ObservableObject`プロトコルに準拠することで、データの変更がUIに自動的に反映されるようになっています。

## ステップ3：　各タスクの表示方法を定義
新規ファイル（ListRow.swift)作成。作成方法はステップ１と同じ。
```swift
import SwiftUI

struct ListRow: View {
    let task: String
    var isCheck: Bool
    
    var body: some View {
        HStack {
            if isCheck {
                Text("☑︎")
                Text(task)
                    .strikethrough()
                    .fontWeight(.ultraLight)
            } else {
                Text("◻︎")
                Text(task)
            }
        }
    }
}

#Preview {
    ListRow(task: "料理", isCheck: true)
}
```
#### コード解説
1. `struct ListRow: View`
   - `ListRow`という構造体を定義し、`View`プロトコルに準拠させています。これはSwiftUIのカスタムビューとなります。

2. プロパティ
   - `let task: String`: タスクの内容を保持する変更不可能なプロパティです。
   - `var isCheck: Bool`: タスクが完了しているかどうかを示す変更可能なプロパティです。

3. `var body: some View`
   - これはViewプロトコルの要求を満たすための計算プロパティです。
   - `some View`は不透明な戻り値型で、具体的な型を隠蔽しています。

4. ビューの構造
   - `HStack`を使用して、水平方向に要素を配置しています。
   - `if isCheck { ... } else { ... }`で、タスクの完了状態に応じて異なる表示を行っています。
   - 完了時：チェックマーク（☑︎）、取り消し線付きのテキスト
   - 未完了時：空のチェックボックス（◻︎）、通常のテキスト

5. テキストスタイリング
   - `.strikethrough()`: テキストに取り消し線を追加します。
   - `.fontWeight(.ultraLight)`: フォントの太さを非常に細くします。

6. プレビュー
   - `#Preview`マクロを使用して、SwiftUIのプレビュー機能を利用しています。
   - `ListRow(task: "料理", isCheck: true)`でサンプルデータを使用してプレビューを生成しています。

この`ListRow`ビューは、タスクリストの各行の表示を担当します。タスクの内容と完了状態に応じて適切な表示を行い、ユーザーにタスクの状態を視覚的に伝えます。

## ステップ4：　新しいタスクの入力フォームを定義
新規ファイル（Draft.swift)作成。作成方法はステップ１と同じ。
```swift
import SwiftUI

struct Draft: View {
    @Binding var taskTitle: String
    var onCommit: () -> Void
    
    var body: some View {
        TextField("タスクを入力してください", text: $taskTitle, onCommit: onCommit)
    }
}

#Preview {
    Draft(taskTitle: .constant(""), onCommit: {})
}
```
#### コード解説

1. `struct Draft: View`
   - `Draft` という名前の構造体を定義し、SwiftUI の `View` プロトコルに準拠させています。
   - これにより、この構造体が SwiftUI のビューとして機能することを示しています。

2. プロパティ
   ```swift
   @Binding var taskTitle: String
   var onCommit: () -> Void
   ```
   - `@Binding var taskTitle: String`
     - `@Binding` 属性は、このプロパティが親ビューの状態とバインドされていることを示します。
     - これにより、`Draft` ビューで `taskTitle` を変更すると、親ビューの対応する値も更新されます。
   - `var onCommit: () -> Void`
     - これは「クロージャ」と呼ばれる、引数を取らず何も返さない関数です。
     - タスクの入力が完了したときに実行される処理を親ビューから受け取るために使用されます。

3. `body` プロパティ
   ```swift
   var body: some View {
       TextField("タスクを入力してください", text: $taskTitle, onCommit: onCommit)
   }
   ```
   - `body` プロパティは `View` プロトコルで要求される項目で、ビューの内容を定義します。
   - ここでは `TextField` を使用しています：
     - 第一引数 `"タスクを入力してください"` は、プレースホルダーテキストです。
     - `text: $taskTitle` で、テキストフィールドの内容を `taskTitle` にバインドしています。
     - `onCommit: onCommit` で、ユーザーが入力を確定したときに実行する処理を指定しています。

4. プレビュー
   ```swift
   #Preview {
       Draft(taskTitle: .constant(""), onCommit: {})
   }
   ```
   - これは SwiftUI のプレビュー機能用のコードです。
   - `.constant("")` は空の文字列の定数バインディングを作成します。
   - `onCommit: {}` は何もしない空のクロージャです。

この `Draft` ビューは、新しいタスクを入力するためのシンプルなテキストフィールドを提供します。親ビューからタスクのタイトルと入力完了時の処理を受け取り、ユーザーの入力を親ビューに反映させる役割を果たします。

このコンポーネントは再利用可能で、様々な場所で新しいタスクの入力に使用できます。また、`@Binding` と `onCommit` クロージャを使用することで、親ビューとの柔軟な連携が可能になっています。

## ステップ5：　アプリのメインビューを定義
ContentView.swift ※これは初期からある
この時点でアプリがクラッシュすることがあります。その場合、ステップ6を行なってください。また、同じことを、`ContentView.swift`ファイルの以下にも施してください。
```swift
#Preview {
    ContentView()
        .environmentObject(UserData()) // ここを追加
}
```
```swift
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var userData: UserData
    @State private var newTaskTitle = ""
    
    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(userData.tasks) { task in
                        Button(action: {
                            guard let index = self.userData.tasks
                                .firstIndex(of: task) else {
                                return
                            }
                            self.userData.tasks[index].checked.toggle()
                            self.userData.saveTasks()
                        }) {
                            ListRow(task: task.title, isCheck: task.checked)
                        }
                    }
                    if self.userData.isEditing {
                        Draft(taskTitle: $newTaskTitle, onCommit: {
                            if !newTaskTitle.isEmpty {
                                let newTask = Task(title: newTaskTitle, checked: false)
                                self.userData.addTask(newTask)
                                self.newTaskTitle = ""
                                self.userData.isEditing = false
                            }
                        })
                    } else {
                        Button(action: {
                            self.userData.isEditing = true
                            self.newTaskTitle = ""
                        }) {
                            Text("+")
                                .font(.title)
                        }
                    }
                }
            }
            .navigationBarTitle("Tasks")
            .navigationBarItems(trailing: Button(action: {
                DeleteTask()
            }) {
                Text("Delete")
            })
        }
    }
    
    func DeleteTask() {
        self.userData.tasks = self.userData.tasks.filter { !$0.checked }
        self.userData.saveTasks()
    }
}

#Preview {
    ContentView()
        .environmentObject(UserData())
}
```
#### コード解説
1. `struct ContentView: View`
   - `ContentView` 構造体を定義し、SwiftUI の `View` プロトコルに準拠させています。

2. プロパティ
   - `@EnvironmentObject var userData: UserData`
     - `UserData` オブジェクトを環境オブジェクトとして宣言しています。これにより、アプリ全体でデータを共有できます。
   - `@State private var newTaskTitle = ""`
     - 新しいタスクのタイトルを保持する状態変数です。

3. `var body: some View`
   - ビューの内容を定義する計算プロパティです。

4. ビューの構造
   - `NavigationView`: ナビゲーション機能を提供します。
   - `VStack`: 垂直方向にビューを積み重ねます。
   - `List`: タスクのリストを表示します。

5. タスクリストの表示
   - `ForEach` ループを使用して、`userData.tasks` の各タスクに対して `Button` を生成しています。
   - 各ボタンのアクションでは、タスクの完了状態を切り替え、変更を保存しています。
   - `ListRow` を使用して、各タスクの表示をカスタマイズしています。

6. 新しいタスクの追加
   - `userData.isEditing` が `true` の場合、`Draft` ビューを表示します。
   - そうでない場合は、「+」ボタンを表示します。

7. ナビゲーションバーの設定
   - `.navigationBarTitle("Tasks")`: ナビゲーションバーのタイトルを設定します。
   - `.navigationBarItems(trailing: ...)`: ナビゲーションバーの右側に「Delete」ボタンを追加します。
   - 
8. `DeleteTask()` 関数
   - チェックされていないタスクだけを残し、チェックされたタスクを削除します。
   - 変更を保存します。

9. プレビュー
    - `ContentView` のプレビューを設定し、`UserData` オブジェクトを環境オブジェクトとして提供しています。

この `ContentView` は、アプリケーションのメイン画面を構成しています。タスクのリスト表示、新しいタスクの追加、タスクの完了状態の変更、完了したタスクの削除など、アプリの主要な機能をこのビューで実現しています。SwiftUI の宣言的な構文を使用して、複雑なUIとインタラクションを簡潔に記述しています。

## ステップ6：　アプリのエントリーポイントを定義し、`UserData`を環境オブジェクトとして提供
TodoApp.swift ※各自で決めたアプリ名のファイルが初期からある
```swift
import SwiftUI

@main
struct TodoAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(UserData())
        }
    }
}
```
#### コード解説
1. `@main`
   - これは、このタイプがアプリケーションのエントリーポイントであることを示す属性です。
   - Swift 5.3以降で導入された機能で、以前の `UIApplicationDelegate` や `NSApplicationDelegate` を使用する方法を置き換えます。

2. `struct TodoAppApp: App`
   - `TodoAppApp` という名前の構造体を定義し、`App` プロトコルに準拠させています。
   - `App` プロトコルは、アプリケーションの構造と動作を定義するための SwiftUI の新しい方法です。

3. `var body: some Scene`
   - `body` プロパティは `App` プロトコルで要求される項目です。
   - `some Scene` は、不透明な戻り値型を示し、具体的な Scene タイプを隠蔽しています。

4. `WindowGroup { ... }`
   - `WindowGroup` は、アプリケーションのメインウィンドウを表すシーンを作成します。
   - これは、iPadOS でのマルチウィンドウサポートなど、プラットフォーム固有の動作を自動的に処理します。

5. `ContentView()`
   - アプリケーションのルートビューとして `ContentView` を指定しています。
   - これは、アプリケーションが起動したときに最初に表示されるビューです。

6. `.environmentObject(UserData())`
   - `environmentObject` モディファイアを使用して、`UserData` のインスタンスをアプリケーション全体で利用可能な環境オブジェクトとして追加しています。
   - これにより、`ContentView` やその子ビューで `@EnvironmentObject` を使用して `UserData` にアクセスできるようになります。

このファイルは、アプリケーションの「エントリーポイント」として機能し、以下の役割を果たします：

- アプリケーションの起動時に最初に実行されるコードを提供します。
- アプリケーションの全体的な構造を定義します。
- ルートビュー（この場合は `ContentView`）を指定します。
- アプリケーション全体で共有される環境オブジェクト（この場合は `UserData`）を設定します。

`TodoAppApp` 構造体は、SwiftUIの `App` プロトコルを使用してアプリケーションのライフサイクルと構造を定義する現代的な方法を示しています。これにより、簡潔で宣言的な方法でアプリケーションのセットアップを行うことができます。

以上がデータの永続化を含む簡単なTodoアプリの開発ステップになります。