見出し画像

ChatGPTに教えてもらえばAIアプリだって作れる様になるはず! 9.Task/To-Do List #機械学習による人間学習

はじめに

このブログでは、ChatGPTで作成した「AIアプリを作れる様になるまでの100個のアプリ案」を実際に作り、学んだことや失敗したこと、感じたことを記していきます。

記事は、簡単なアプリ紹介、コードのポイント、感想、ChatGPTによるStep by Stepガイドの順番に記載しています。

アプリを作るにはChatGPTによるStep by Stepガイドをフォローするだけで十分ですが、このアプリを作る上で学べることが分かりにくいので、コードのポイント、感想は参考になることがあるかもしれません。

(※本ブログはマガジン「ChatGPTに教えてもらえばにまとめてあります。)


今回のアプリ "9.Task/To-Do List"


概要 : SwiftUIのリストでタスクを追加、削除、完了マークをつける。

狙い : リスト管理とデータバインディングに焦点を当てる。

9個目のアプリはTask/To-Do Listです。シンプルな見た目で簡単に作れそうですが、改めて機能を考えてみると、テキストフィールドにタスクを記入、保存し、チェックボックスを付けて、チェックの有無を管理、終わったタスクは削除、という流れ?フロー?を考えると挫折も視野に入ってきます。

ちゃんと動くか分からないまま、プログラムを書いていくのではなく、まず実際に動くまでChatGPTでプログラムを修正してから、腰を据えてプログラムの学習を進めるというのは学習効率的にもメンタル的にもメリットがありそうな気がします。

コードのポイント

なぜ、Taskをstructで定義しているのか?


struct Task: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
}

上記の様にTaskは、変数varや配列[ ]ではなく、structで定義していました。多分、複数の変数を管理するのに便利だからだと予測出来ますが、確認のため、ChatGPTに聞いてみました。

Taskをstructで定義しているのは、複数の変数を持たせるためですか?

以下、ChatGPTの回答

「Task」を struct で定義している主な理由は、以下のように複数のプロパティをまとめて管理するためです。例えば、タスクのタイトル、完了状態、ユニークIDなどを一つの型(構造体)にまとめておくと、コードが整理されて扱いやすくなります。

なぜ構造体 (struct) を使うのか

  1. 複数のプロパティをまとめる

    • 例: title, isCompleted, id など、タスクの情報をひとつの型で扱える。

    • それぞれを別々の変数として持つより、1つの Task 型としてまとめたほうが見通しが良い。

  2.  SwiftUIのListで管理しやすい

    • Task が Identifiable を実装すると、id: UUID などでユニークに識別でき、List や ForEach と組み合わせやすい。

  3.  値型 (struct) のメリット

    • Swift では、データモデルに構造体をよく使います。

    • クラス (class) と比べて、スレッドセーフやコピー動作などの面で扱いやすい場面が多い。

  4.  状態を簡単に扱える

    • struct は値セマンティクスを持ち、変更が加わるたびに新しいコピーを作り出すため、SwiftUI の再描画や @State 管理との相性が良い。

まとめ

  • 複数の関連情報をひとまとめにするために、Task という構造体(複数プロパティを持つ型)を定義している。

  • これにより「タイトル」「完了状態」「ID」をはじめとするタスク情報を一元管理でき、SwiftUIのListや識別がスムーズになる。

  • struct はSwiftUIと相性が良い値型なので、今回のようなシンプルなデータモデルにはよく使われます。


とりあえず、複数の関連情報をひとまとめにするためにはstructを使っておけば間違いなさそうです。

Taskは下記の様にこの後、tasks変数の型となり、func addTask()の中でTask(title: newTaskTitle, isCompleted: false)と関数の様に書いて、Task内の値を上書きして利用されていました。

つまり、structで定義された変数は、型や関数の様に取り扱うことが出来るみたいです。

import SwiftUI

struct ContentView: View {
    @State private var tasks: [Task] = []
    func addTask() {
        guard !newTaskTitle.isEmpty else { return }
        let newTask = Task(title: newTaskTitle, isCompleted: false)
        tasks.append(newTask)
        newTaskTitle = ""
    }


なぜ TaskRow をstructで作るのか?


コードを見てみるとTaskRowというstructが用意されていました。リストの表示部分ではTaskRowをForEachで呼び出して、UIを表示させています。よく見るとどちらもstructですが、その後に続く文字がTask: IdentifiableとTaskRow: Viewで少し異なります。同じstructでも何か違いがありそうです。こちらもChatGPTに聞いてみます。

TaskRowビューを作成の部分に関してもう少し詳しく説明してください。

以下、ChatGPTの回答

以下では、TaskRowビューを作成する意図やコードの詳細について、もう少し深い解説を示します。

1. なぜ TaskRow を作るのか

a. 再利用性と可読性の向上

  • 1つの行(row)の表示を独立したカスタムビューとして切り出すことで、見通しがよくなり、コードの重複を防げる。

  • SwiftUIではListやForEachを使うとき、「各行の見た目をどうするか」をカプセル化しておくと可読性が上がる。

b. データと見た目の分離

  • Task(データモデル)とTaskRow(UI部品)を分けておくと、UIの修正(例: チェックマークのアイコンを変える)やデータ構造の修正(例: Taskに説明文を追加)に対応しやすい。

c. ユニットテストや拡張性

  • 個別のViewとして切り出すことで、あとからテストを書きやすくなる場合や、追加情報(例えば期日や優先度)を同じ行に表示するなどの拡張が容易になる。

2. TaskRow のコード詳細

struct TaskRow: View {
    let task: Task
    let toggleCompletion: (Task) -> Void

    var body: some View {
        HStack {
            // チェックボックス部分
            Image(systemName: task.isCompleted ? "checkmark.square" : "square")
                .foregroundColor(task.isCompleted ? .green : .gray)
                .onTapGesture {
                    // チェック部分をタップしたら、外部に用意されたtoggle関数を呼ぶ
                    toggleCompletion(task)
                }

            // タスクのタイトル表示
            Text(task.title)
                // 完了しているなら打ち消し線を入れる
                .strikethrough(task.isCompleted, color: .gray)
        }
    }
}

a. 引数に task と toggleCompletion を取る理由

  1. task: Taskの具体的なデータ

    • 例: title: "買い物に行く", isCompleted: false

    • TaskRow は「このタスクの見た目」を作る役割なので、どのタスクを表示すべきかを受け取る必要がある。

  2. toggleCompletion: (Task) -> Void

    • 「チェックボックスをタップしたときにどうするか?」を上位のContentViewなどに任せるためのコールバック。

    • TaskRowはUI部品として「タスクの完了/未完了を切り替えるアクションを発火」するのみで、実際のtasks配列の更新や保存は上位で行う(単一責任の原則)。

b. Image(systemName:) の仕組み

  • SFSymbolsを利用し、チェックボックスやその他アイコンをシンプルに表示できる。

  • "square"(□)や "checkmark.square"(✅の□)を条件分岐して表示することで、タスクの完了状態を視覚的に表現。

c. .onTapGesture { ... }

  • SwiftUIのジェスチャー修飾子。

  • ここではアイコン(チェックボックス部分)をタップしたらtoggleCompletion(task)を呼び出す。

d. .strikethrough(task.isCompleted, color: .gray)

  • Textの修飾。

  • isCompleted == trueなら打ち消し線(ストライクスルー)を引き、視覚的に完了を示す。

3. TaskRow が呼び出される側

List {
    ForEach(tasks) { task in
        TaskRow(task: task, toggleCompletion: toggleCompletion)
    }
    .onDelete(perform: deleteTask)
}
  • 例: ForEachでtasksを回して1行ごとにTaskRowを返す。

  • toggleCompletionはContentViewが保持する「タスクの完了状態を実際に変更する関数」。

  • TaskRow内の.onTapGestureから呼び出され、tasks[index].isCompleted.toggle()など実際の配列更新を行う仕組み。

4. 実運用上の工夫

  1. 見た目の拡張

    • TaskRowにアイコンや日付、優先度を表示したり、行全体をタップして完了にするなど柔軟にUIを足せる。

  2. スワイプ操作

    • List自体に.onDeleteや.swipeActionsを追加すれば、スワイプで削除/編集。

    • 行の個別カスタマイズ(例: 右スワイプで「完了」)も可能。

  3. 管理対象を増やす

    • Task構造体にタグや期日、メモなど新フィールドを追加すれば、TaskRowでも表示を増やすだけ。

    • コードの再利用・拡張が容易になる。

まとめ

  • TaskRow は1つのタスク行を表示・操作する小さなUIコンポーネント。

  • タスクデータ(task)と完了トグル処理(toggleCompletion) を外部から渡すことで、責務が明確になる。

  • Row単位でUIを分ける利点は「読みやすさ」「拡張しやすさ」「SwiftUIのListとの相性」などが挙げられる。

このように、「1行を表示するビュー」を部品化しておくと、あとから機能追加や見た目の変更をする際にコードのメンテナンスがとても楽になります。


TaskとTaskRowをデータモデルとUI部品に分けていたんですね。おそらく一つにすることも可能だと思いますが、後から見たり、修正することを考えると一つの大きなstructの中にデータモデルとUI部品が混在しているより、役割によって分かれている方が確かに便利そうです。

感想

今回のアプリがこれまでと異なるのは、structを使ってオブジェクトを定義した点です。

これまでのアプリは基本的にContentViewの中にTextやImage、Textfieldなどのstructで定義していないオブジェクトに.onAppearなどのメソッドを付けて、さらにこれらメソッドにfunctionを埋め込んでアプリを構成する形でした。structでオブジェクトを定義し、複数のプロパティを持たせる方法は初めて学びました。

この方法は、ゲームキャラクターに性別や体力、特技などのパラメータを付与して名前を付ける様なイメージで、音楽アプリでお気に入り曲のリストを作ったり、カレンダーの予定リストを作ったりと、多様な応用が考えられる拡張性が高い重要な機能を学んだことになります。


ChatGPTの回答


以下は、リストの9番目のプロジェクトにあたる 「Task/To-Do リスト」アプリを作成するためのステップバイステップガイドです。リスト管理、データバインディング、ステート管理を復習しながら、タスクを追加、削除、完了状態を切り替えできる機能を実装します。

1. 新しい SwiftUI プロジェクトを作成

  1. Xcodeを起動

    • アプリケーションフォルダからXcodeを探して起動します。

  2. 新しいプロジェクトを作成

    • 「Create a new Xcode project…」を選択(あるいは、File > New > Project… を選択)。

  3. テンプレートの選択

    • 左側の iOS で App を選択し、「Next」をクリック。

  4. 設定

    • Product Name: TaskListApp (名称は自由)

    • Team: Apple Developerアカウント、または “None” (ローカルテスト用)

    • Organization Identifier: 例: com.yourname

    • Interface: SwiftUI

    • Language: Swift

    • 「Next」をクリックし、保存場所を指定して「Create」。

2. タスクモデルを定義

まずは「タスク(To-Do)」を表す構造体を用意します。

struct Task: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
}
  • id: それぞれのタスクを一意に識別するための UUID。SwiftUIのListで必要。

  • title: タスクの内容やタイトル。

  • isCompleted: タスクが完了済みかどうかをブール値で保持。

このコードは Task.swift など別ファイルでも、ContentView.swift の先頭でもOKです。

3. SwiftUIレイアウトを構築

以下の要素を配置します。

  1. TextField: 新しいタスクを入力するテキストフィールド

  2. Button: 追加ボタンで tasks の配列に新タスクを追加

  3. List: タスクを一覧表示

  4. タスクの削除機能(スワイプ)

  5. 完了状態を切り替える仕組み

ContentView.swift の基本構成

import SwiftUI

struct ContentView: View {
    @State private var tasks: [Task] = []
    @State private var newTaskTitle: String = ""

    var body: some View {
        NavigationView {
            VStack {
                // 1) 新しいタスクの追加
                HStack {
                    TextField("Enter new task...", text: $newTaskTitle)
                        .textFieldStyle(RoundedBorderTextFieldStyle())

                    Button(action: addTask) {
                        Image(systemName: "plus.circle.fill")
                            .font(.title)
                            .foregroundColor(.blue)
                    }
                }
                .padding()

                // 2) タスクリスト
                List {
                    ForEach(tasks) { task in
                        TaskRow(task: task, toggleCompletion: toggleCompletion)
                    }
                    .onDelete(perform: deleteTask)
                }
                .listStyle(PlainListStyle())
            }
            .navigationTitle("Task Manager")
        }
    }

    // MARK: - メソッド
    func addTask() {
        guard !newTaskTitle.isEmpty else { return }
        let newTask = Task(title: newTaskTitle, isCompleted: false)
        tasks.append(newTask)
        newTaskTitle = ""
    }

    func toggleCompletion(_ task: Task) {
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index].isCompleted.toggle()
        }
    }

    func deleteTask(at offsets: IndexSet) {
        tasks.remove(atOffsets: offsets)
    }
}

解説

  • @State private var tasks: [Task] = []: タスクの配列。これがListと同期される。

  • @State private var newTaskTitle: String: 新タスクのタイトルを入力するテキストフィールドのバインド先。

  • TextField("Enter new task...", text: $newTaskTitle): 新しいタスク名の入力欄。

  • Button(action: addTask): 「+」アイコンを押すと addTask() が呼ばれ、tasks に新タスクを追加。

  • List { ForEach(tasks) }: タスクリストを表示。

  • onDelete(perform: deleteTask): スワイプで削除可能。

  • toggleCompletion(_ task:): タップで完了/未完了を切り替えるロジック。

4. TaskRowビューを作成

各タスクのチェックボックスやテキストをまとめるビュー。

struct TaskRow: View {
    let task: Task
    let toggleCompletion: (Task) -> Void

    var body: some View {
        HStack {
            Image(systemName: task.isCompleted ? "checkmark.square" : "square")
                .foregroundColor(task.isCompleted ? .green : .gray)
                .onTapGesture {
                    toggleCompletion(task)
                }

            Text(task.title)
                .strikethrough(task.isCompleted, color: .gray)
        }
    }
}

解説

  • SFSymbolsの**“checkmark.square”** / **“square”**を使って簡易的なチェックボックス表示。

  • タスクが完了なら、未完了なら灰色

  • タスクタイトルに**strikethrough**を付与し、isCompleted == true なら取り消し線を表示。

5. タスクを永続化したい場合(オプション)

デフォルトでは、アプリを再起動すると tasks の内容が失われます。永続化するには以下の方法があります。

a) UserDefaults

JSONエンコード/デコードを使ってタスク配列を保存・読み込み。

func saveTasks() {
    if let encoded = try? JSONEncoder().encode(tasks) {
        UserDefaults.standard.set(encoded, forKey: "tasksKey")
    }
}

func loadTasks() {
    if let data = UserDefaults.standard.data(forKey: "tasksKey"),
       let decoded = try? JSONDecoder().decode([Task].self, from: data) {
        tasks = decoded
    }
}

// ContentViewのonAppearなどでloadTasks()を呼び、addTaskやtoggleCompletionでもsaveTasks()を呼ぶ。

b) Core Data

より大規模なデータやリレーションを扱うならCore Dataが便利。
@FetchRequestやNSManagedObjectを使ってタスク管理可能。ただしセットアップはやや高度。

6. アプリを実行

  1. シミュレーター選択: 例:iPhone 14

  2. Runボタン: ビルド&実行

  3. タスクを追加: テキストフィールドにタイトルを入力し、+ボタンを押す

  4. チェック: チェックボックスで完了状態をトグル

  5. 削除: スワイプして削除する

7. 追加機能のアイデア

  1. ソート & フィルタ

    • 完了したタスクを下に移動、または非表示にする

    • 「All」「Active」「Completed」でセグメント切り替え

  2. 期日やリマインダー

    • 日付を設定し、ローカル通知でリマインドする機能

  3. 複数リスト / カテゴリ

    • 各リストにタスクを分ける

    • フォルダ・プロジェクト別など

  4. Python バックエンドとの連携

    • REST APIを使い、サーバー上でタスクを管理

    • 複数端末で同期や共同編集

  5. UIの拡張

    • カスタムアイコン、色分け、アニメーションなど

最終コードまとめ

import SwiftUI

struct Task: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
}

struct ContentView: View {
    @State private var tasks: [Task] = []
    @State private var newTaskTitle: String = ""

    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    TextField("Enter new task...", text: $newTaskTitle)
                        .textFieldStyle(RoundedBorderTextFieldStyle())

                    Button(action: addTask) {
                        Image(systemName: "plus.circle.fill")
                            .font(.title)
                            .foregroundColor(.blue)
                    }
                }
                .padding()

                List {
                    ForEach(tasks) { task in
                        TaskRow(task: task, toggleCompletion: toggleCompletion)
                    }
                    .onDelete(perform: deleteTask)
                }
            }
            .navigationTitle("Task Manager")
        }
    }

    func addTask() {
        guard !newTaskTitle.isEmpty else { return }
        let newTask = Task(title: newTaskTitle, isCompleted: false)
        tasks.append(newTask)
        newTaskTitle = ""
    }

    func toggleCompletion(_ task: Task) {
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index].isCompleted.toggle()
        }
    }

    func deleteTask(at offsets: IndexSet) {
        tasks.remove(atOffsets: offsets)
    }
}

struct TaskRow: View {
    let task: Task
    let toggleCompletion: (Task) -> Void

    var body: some View {
        HStack {
            Image(systemName: task.isCompleted ? "checkmark.square" : "square")
                .foregroundColor(task.isCompleted ? .green : .gray)
                .onTapGesture {
                    toggleCompletion(task)
                }

            Text(task.title)
                .strikethrough(task.isCompleted, color: .gray)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

これで、タスクの追加、削除、完了切り替えができるシンプルなTo-Doリストアプリが完成です。
必要に応じてローカル保存やサーバー同期の機能を拡張してみてください。


オプション機能:ソート & フィルタ機能


概要

  • タスクが保存され、アプリをクローズしても消えません。

  • テェックボックスにテェックを入れたタスクはリストの下部に移動します。

  • 進行中タスク、完了タスクでフィルタがかけられます。

注:struct TaskをTask.swiftとして別ファイルに作成しています。ビルドする際は、バンドル内に両方保存する必要があります。

ContentView.swift

import SwiftUI

enum FilterType: String, CaseIterable {
    case all = "All"
    case active = "Active"
    case completed = "Completed"
}

struct ContentView: View {
    @State private var tasks: [Task] = []
    @State private var newTaskTitle: String = ""
    @State private var selectedFilter: FilterType = .all
    
    var body: some View {
        NavigationView {
            VStack {
                Picker("Filter", selection: $selectedFilter) {
                    ForEach(FilterType.allCases, id: \.self) { filterCase in
                        Text(filterCase.rawValue).tag(filterCase)
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
                .padding()
                
                HStack {
                    TextField("Enter new task...", text: $newTaskTitle)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                    Button(action: addTask) {
                        Image(systemName: "plus.square.fill")
                            .font(.title)
                            .foregroundColor(.blue)
                    }
                }
                .padding()
                
                List {
                    ForEach(filteredTasks) { task in
                        TaskRow(task: task, toggleCompletion: toggleCompletion)
                    }
                    .onDelete(perform: deleteTask)
                }
//                .listStyle(PlainListStyle())
            }
            .navigationTitle("Task Manager")
            .onAppear{
                loadTasks()
            }
        }
    }
    
    var filteredTasks: [Task] {
        let filtered: [Task]
        switch selectedFilter {
        case .all: filtered = tasks
        case .active: filtered = tasks.filter { !$0.isCompleted }
        case .completed: filtered = tasks.filter { $0.isCompleted }
        }
        
        return filtered.sorted { t1, t2 in
            (t1.isCompleted == false && t2.isCompleted == true)
        }
    }
    
    
    func addTask() {
        hideKeyboard()
        // "!"
        // call Task.swift struct
        guard !newTaskTitle.isEmpty else { return }
        tasks.append(Task(title: newTaskTitle, isCompleted: false))
        newTaskTitle = ""
        saveTasks()
    }
    
    func toggleCompletion(_ task: Task) {
        // .firstIndex (where: { $0.id == task.id })
        // .toggle()
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index].isCompleted.toggle()
        }
        saveTasks()
    }
    
    func deleteTask(at offsets: IndexSet) {
        tasks.remove(atOffsets: offsets)
        saveTasks()
    }
    
    func hideKeyboard() {
        // This sends a resignFirstResponder action so all text fields commit any pending input
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
                                        to: nil,
                                        from: nil,
                                        for: nil)
    }
    
    func saveTasks() {
        if let encoded = try? JSONEncoder().encode(tasks) {
            UserDefaults.standard.set(encoded, forKey: "tasksKey")
        }
    }
    
    func loadTasks() {
        if let data = UserDefaults.standard.data(forKey: "tasksKey"),
           let decoded = try? JSONDecoder().decode([Task].self, from: data) {
            tasks = decoded
        }
    }
}

struct TaskRow: View {
    let task: Task
    let toggleCompletion: (Task) -> Void
    
    var body: some View {
        HStack {
            Image(systemName: task.isCompleted ? "checkmark.square" : "square")
                .foregroundColor(task.isCompleted ? .green : .gray)
                .onTapGesture {
                    toggleCompletion(task)
                }
            
            Text(task.title)
                .strikethrough(task.isCompleted, color: .gray)
        }
    }
}

#Preview {
    ContentView()
}

Task.swift

import SwiftUI

struct Task: Identifiable, Codable {
    var id = UUID()
    var title: String
    var isCompleted: Bool
}

いいなと思ったら応援しよう!