今回は、完全初心者向けなので、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
}
}
コード解説
-
struct Task: Identifiable, Codable, Equatable
-
Task
という構造体を定義しています。 -
Identifiable
プロトコルに準拠することで、各タスクに一意の識別子を持たせます。これは SwiftUI の List や ForEach で使用する際に便利です。 -
Codable
プロトコルに準拠することで、この構造体を JSON などにエンコード/デコードできるようになります。これはデータの保存や読み込みに使用されます。 -
Equatable
プロトコルに準拠することで、2つの Task インスタンスが等しいかどうかを比較できるようになります。
-
-
プロパティ
-
let id: UUID
: 各タスクの一意の識別子です。let
で宣言されているため、一度設定されると変更できません。 -
var title: String
: タスクのタイトルを保持します。var
で宣言されているため、後から変更可能です。 -
var checked: Bool
: タスクが完了したかどうかを示すフラグです。これもvar
で、変更可能です。
-
-
イニシャライザ
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、タイトル、完了状態を持ち、簡単に識別、保存、比較することができます。
ステップ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]
}
}
コード解説
-
クラス定義:
class UserData: ObservableObject
-
ObservableObject
プロトコルに準拠することで、このクラスのインスタンスの変更をSwiftUIビューが監視できるようになります。
-
-
プロパティ:
@Published var tasks: [Task] = [] @Published var isEditing: Bool = false @Published var draftTitle: String = ""
-
@Published
属性は、プロパティの変更をObservableObjectが自動的に発行するようにします。 -
tasks
はタスクのリストを保持します。 -
isEditing
は新しいタスクの追加モードかどうかを示します。 -
draftTitle
は新しいタスクのタイトルの下書きを保持します。
-
-
初期化:
init() { loadTasks() }
- インスタンス作成時に
loadTasks()
を呼び出し、保存されているタスクを読み込みます。
- インスタンス作成時に
-
メソッド:
-
resetDraft()
: 新しいタスクの入力をリセットします。 -
addTask(_:)
: 新しいタスクを追加し、保存します。 -
saveTasks()
: タスクをJSONとしてファイルに保存します。 -
loadTasks()
: ファイルからタスクを読み込みます。 -
getDocumentsDirectory()
: アプリのドキュメントディレクトリのURLを取得します。
-
-
エラーハンドリング:
-
saveTasks()
とloadTasks()
メソッドでは、do-catch
文を使用してエラーを処理しています。
-
-
データの永続化:
-
JSONEncoder
とJSONDecoder
を使用して、タスクデータを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)
}
コード解説
-
struct ListRow: View
-
ListRow
という構造体を定義し、View
プロトコルに準拠させています。これはSwiftUIのカスタムビューとなります。
-
-
プロパティ
-
let task: String
: タスクの内容を保持する変更不可能なプロパティです。 -
var isCheck: Bool
: タスクが完了しているかどうかを示す変更可能なプロパティです。
-
-
var body: some View
- これはViewプロトコルの要求を満たすための計算プロパティです。
-
some View
は不透明な戻り値型で、具体的な型を隠蔽しています。
-
ビューの構造
-
HStack
を使用して、水平方向に要素を配置しています。 -
if isCheck { ... } else { ... }
で、タスクの完了状態に応じて異なる表示を行っています。 - 完了時:チェックマーク(☑︎)、取り消し線付きのテキスト
- 未完了時:空のチェックボックス(◻︎)、通常のテキスト
-
-
テキストスタイリング
-
.strikethrough()
: テキストに取り消し線を追加します。 -
.fontWeight(.ultraLight)
: フォントの太さを非常に細くします。
-
-
プレビュー
-
#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: {})
}
コード解説
-
struct Draft: View
-
Draft
という名前の構造体を定義し、SwiftUI のView
プロトコルに準拠させています。 - これにより、この構造体が SwiftUI のビューとして機能することを示しています。
-
-
プロパティ
@Binding var taskTitle: String var onCommit: () -> Void
-
@Binding var taskTitle: String
-
@Binding
属性は、このプロパティが親ビューの状態とバインドされていることを示します。 - これにより、
Draft
ビューでtaskTitle
を変更すると、親ビューの対応する値も更新されます。
-
-
var onCommit: () -> Void
- これは「クロージャ」と呼ばれる、引数を取らず何も返さない関数です。
- タスクの入力が完了したときに実行される処理を親ビューから受け取るために使用されます。
-
-
body
プロパティvar body: some View { TextField("タスクを入力してください", text: $taskTitle, onCommit: onCommit) }
-
body
プロパティはView
プロトコルで要求される項目で、ビューの内容を定義します。 - ここでは
TextField
を使用しています:- 第一引数
"タスクを入力してください"
は、プレースホルダーテキストです。 -
text: $taskTitle
で、テキストフィールドの内容をtaskTitle
にバインドしています。 -
onCommit: onCommit
で、ユーザーが入力を確定したときに実行する処理を指定しています。
- 第一引数
-
-
プレビュー
#Preview { Draft(taskTitle: .constant(""), onCommit: {}) }
- これは SwiftUI のプレビュー機能用のコードです。
-
.constant("")
は空の文字列の定数バインディングを作成します。 -
onCommit: {}
は何もしない空のクロージャです。
この Draft
ビューは、新しいタスクを入力するためのシンプルなテキストフィールドを提供します。親ビューからタスクのタイトルと入力完了時の処理を受け取り、ユーザーの入力を親ビューに反映させる役割を果たします。
このコンポーネントは再利用可能で、様々な場所で新しいタスクの入力に使用できます。また、@Binding
と onCommit
クロージャを使用することで、親ビューとの柔軟な連携が可能になっています。
ステップ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())
}
コード解説
-
struct ContentView: View
-
ContentView
構造体を定義し、SwiftUI のView
プロトコルに準拠させています。
-
-
プロパティ
-
@EnvironmentObject var userData: UserData
-
UserData
オブジェクトを環境オブジェクトとして宣言しています。これにより、アプリ全体でデータを共有できます。
-
-
@State private var newTaskTitle = ""
- 新しいタスクのタイトルを保持する状態変数です。
-
-
var body: some View
- ビューの内容を定義する計算プロパティです。
-
ビューの構造
-
NavigationView
: ナビゲーション機能を提供します。 -
VStack
: 垂直方向にビューを積み重ねます。 -
List
: タスクのリストを表示します。
-
-
タスクリストの表示
-
ForEach
ループを使用して、userData.tasks
の各タスクに対してButton
を生成しています。 - 各ボタンのアクションでは、タスクの完了状態を切り替え、変更を保存しています。
-
ListRow
を使用して、各タスクの表示をカスタマイズしています。
-
-
新しいタスクの追加
-
userData.isEditing
がtrue
の場合、Draft
ビューを表示します。 - そうでない場合は、「+」ボタンを表示します。
-
-
ナビゲーションバーの設定
-
.navigationBarTitle("Tasks")
: ナビゲーションバーのタイトルを設定します。 -
.navigationBarItems(trailing: ...)
: ナビゲーションバーの右側に「Delete」ボタンを追加します。
-
-
DeleteTask()
関数- チェックされていないタスクだけを残し、チェックされたタスクを削除します。
- 変更を保存します。
-
プレビュー
-
ContentView
のプレビューを設定し、UserData
オブジェクトを環境オブジェクトとして提供しています。
-
この ContentView
は、アプリケーションのメイン画面を構成しています。タスクのリスト表示、新しいタスクの追加、タスクの完了状態の変更、完了したタスクの削除など、アプリの主要な機能をこのビューで実現しています。SwiftUI の宣言的な構文を使用して、複雑なUIとインタラクションを簡潔に記述しています。
ステップ6: アプリのエントリーポイントを定義し、UserData
を環境オブジェクトとして提供
TodoApp.swift ※各自で決めたアプリ名のファイルが初期からある
import SwiftUI
@main
struct TodoAppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(UserData())
}
}
}
コード解説
-
@main
- これは、このタイプがアプリケーションのエントリーポイントであることを示す属性です。
- Swift 5.3以降で導入された機能で、以前の
UIApplicationDelegate
やNSApplicationDelegate
を使用する方法を置き換えます。
-
struct TodoAppApp: App
-
TodoAppApp
という名前の構造体を定義し、App
プロトコルに準拠させています。 -
App
プロトコルは、アプリケーションの構造と動作を定義するための SwiftUI の新しい方法です。
-
-
var body: some Scene
-
body
プロパティはApp
プロトコルで要求される項目です。 -
some Scene
は、不透明な戻り値型を示し、具体的な Scene タイプを隠蔽しています。
-
-
WindowGroup { ... }
-
WindowGroup
は、アプリケーションのメインウィンドウを表すシーンを作成します。 - これは、iPadOS でのマルチウィンドウサポートなど、プラットフォーム固有の動作を自動的に処理します。
-
-
ContentView()
- アプリケーションのルートビューとして
ContentView
を指定しています。 - これは、アプリケーションが起動したときに最初に表示されるビューです。
- アプリケーションのルートビューとして
-
.environmentObject(UserData())
-
environmentObject
モディファイアを使用して、UserData
のインスタンスをアプリケーション全体で利用可能な環境オブジェクトとして追加しています。 - これにより、
ContentView
やその子ビューで@EnvironmentObject
を使用してUserData
にアクセスできるようになります。
-
このファイルは、アプリケーションの「エントリーポイント」として機能し、以下の役割を果たします:
- アプリケーションの起動時に最初に実行されるコードを提供します。
- アプリケーションの全体的な構造を定義します。
- ルートビュー(この場合は
ContentView
)を指定します。 - アプリケーション全体で共有される環境オブジェクト(この場合は
UserData
)を設定します。
TodoAppApp
構造体は、SwiftUIの App
プロトコルを使用してアプリケーションのライフサイクルと構造を定義する現代的な方法を示しています。これにより、簡潔で宣言的な方法でアプリケーションのセットアップを行うことができます。
以上がデータの永続化を含む簡単なTodoアプリの開発ステップになります。