0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

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

ステップ1: タスクのデータモデルを定義

⌘ + N -> SwiftUI View -> UserTask.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. イニシャライザ

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

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

ステップ2: タスクデータの管理と永続化

新規ファイル(UserData.swift)作成。作成方法はステップ1と同じ。

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. クラス定義:

    class UserData: ObservableObject
    
    • ObservableObjectプロトコルに準拠することで、このクラスのインスタンスの変更をSwiftUIビューが監視できるようになります。
  2. プロパティ:

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

    init() {
        loadTasks()
    }
    
    • インスタンス作成時にloadTasks()を呼び出し、保存されているタスクを読み込みます。
  4. メソッド:

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

    • saveTasks()loadTasks()メソッドでは、do-catch文を使用してエラーを処理しています。
  6. データの永続化:

    • JSONEncoderJSONDecoderを使用して、タスクデータをJSONフォーマットで保存・読み込みします。
    • ファイルの保存・読み込みにはFileManagerを使用しています。

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

ステップ3: 各タスクの表示方法を定義

新規ファイル(ListRow.swift)作成。作成方法はステップ1と同じ。

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)作成。作成方法はステップ1と同じ。

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. プロパティ

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

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

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

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

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

ステップ5: アプリのメインビューを定義

ContentView.swift ※これは初期からある
この時点でアプリがクラッシュすることがあります。その場合、ステップ6を行なってください。また、同じことを、ContentView.swiftファイルの以下にも施してください。

#Preview {
    ContentView()
        .environmentObject(UserData()) // ここを追加
}
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.isEditingtrue の場合、Draft ビューを表示します。
    • そうでない場合は、「+」ボタンを表示します。
  7. ナビゲーションバーの設定

    • .navigationBarTitle("Tasks"): ナビゲーションバーのタイトルを設定します。
    • .navigationBarItems(trailing: ...): ナビゲーションバーの右側に「Delete」ボタンを追加します。
  8. DeleteTask() 関数

    • チェックされていないタスクだけを残し、チェックされたタスクを削除します。
    • 変更を保存します。
  9. プレビュー

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

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

ステップ6: アプリのエントリーポイントを定義し、UserDataを環境オブジェクトとして提供

TodoApp.swift ※各自で決めたアプリ名のファイルが初期からある

import SwiftUI

@main
struct TodoAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(UserData())
        }
    }
}

コード解説

  1. @main

    • これは、このタイプがアプリケーションのエントリーポイントであることを示す属性です。
    • Swift 5.3以降で導入された機能で、以前の UIApplicationDelegateNSApplicationDelegate を使用する方法を置き換えます。
  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アプリの開発ステップになります。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?