SwiftUIとSwiftDataで簡単todoアプリ
対象読者
- SwiftUI触り始めた人
- プログラミングに興味がある人
ある程度知識のある方にはこの記事は退屈かもしれません…
環境
system_profiler SPHardwareDataType
Hardware:
Hardware Overview:
Model Name: MacBook Pro
Model Identifier: Mac14,7
Chip: Apple M2
Total Number of Cores: 8 (4 performance and 4 efficiency)
Memory: 16 GB
以下略
xcodebuild -version
Xcode 15.4
Build version 15F31d
swift --version
swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0
どんなものを作るか
完成品のスクショを貼ります
実機にビルドしたので前に試していたSwiftDataが残っていました
右上のプラスマークを押すとtodo項目が追加できます
プラスマークを押すと下からシートが出てきて
todoの内容と重要度を選択してSaveボタンを押すとSwiftDataに保存するようにします
こんな感じ
todoアプリ作成
まずはxcodeでプロジェクトの作成から
新しいプロジェクト作成から「App」→「Next」
- アプリ名入力
- SwiftData選択
- Next
これで雛形が作成されます
最初のコード説明
プロジェクトが作成されると以下のようなファイルができます
ZennTodoApp
これが一番最初の起点のファイルです
今回は特に触る必要はありません
このファイルのコードを見てみるとContentViewを生成しているので
なんとなくContentViewが起点になっていることがわかりますね
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
ContentView
最初はプラスマークを押すと時間が追加されるようなサンプルがあります
これを参考にしてtodoアプリを作成していきます
ですが色々コードがあって邪魔なので以下のように減らします
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
var body: some View {
Text("hello zenn!")
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
}
Item
import Foundation
import SwiftData
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
なんかクラスが作られて「@Model」でアノテーションをされています
なんとなくDBで使いそうな気がしますよね
ここでは「timestamp」という変数を宣言して初期化しているのがわかります
todoアプリではタイトルなどの項目も必要になってくるので後でここを編集します
Assets
ここは画像などを置いておくところです
flutterやっている人ならassetsでイメージが湧くはず…
アプリのアイコンなどここに格納するだけで簡単に設定できるので超便利です
コードを触っていく
Item.swiftの編集
まずは先ほどのItem.swiftを編集してSwiftDataに保存するデータを定義します
import Foundation
import SwiftData
@Model
final class Item {
var id: UUID
var taskName: String
var priority: String
var timestamp: Date
init(id: UUID, taskName: String, priority: String, timestamp: Date) {
self.id = id
self.taskName = taskName
self.priority = priority
self.timestamp = timestamp
}
}
Item.swiftの中身は以下です
- idをUUIDで宣言
→ UUIDはランダムで固有のIDを作成してくれます - taskName
→ todoアプリに保存する項目名です - priority
→ todo項目の重要度を保存するための変数です。重要度「低」, 「中」, 「高」で保存しようと思います - timestamp 作成時間とか後で役立ちそうなので宣言しておきます
init()のところはxcodeで「init」と入力すると初期化のコードが自動で生成されるので超便利です
こんな感じで
SwiftDataに保存していくデータはざっとこんな感じにしておきます
後で必要な項目があればここに追記するだけなので
AddTodoView.swift
todoアプリでボタンを押すと下から出てくるシートの画面を作成します
「AddTodoView」というファイルを作成します
(面倒なのでoption+ドラッグでContentViewファイルをコピーします)
import SwiftUI
import SwiftData
struct AddTodoView: View {
// 変数とか宣言する所
@FocusState private var isTextFieldFocused: Bool
@Environment(\.dismiss) private var dismiss
@State private var taskName: String = ""
@State private var priority: String = "低"
@State private var showAlert = false
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
let priorities = ["低","中","高"]
// 画面作成する所
var body: some View {
NavigationStack {
VStack {
TextField("todo here", text: $taskName)
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
.padding(.horizontal)
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
.contentShape(Rectangle())
.onTapGesture {
isTextFieldFocused = true
}
.focused($isTextFieldFocused)
Picker("優先度", selection: $priority) {
ForEach (priorities, id: \.self) {
Text($0)
}
}
.pickerStyle(SegmentedPickerStyle())
.frame(height: 50)
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
HStack {
Button(action: addItem) {
HStack {
Image(systemName: "checkmark.circle.fill")
Text("Save")
}
.fontWeight(.medium)
.padding(.vertical, 10)
.padding(.horizontal, 20)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.4), radius: 5, x: 0, y: 5)
}
.frame(width: 150)
}
Spacer()
}
.padding()
.navigationTitle("New Todo")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
Label("Back", systemImage: "xmark.app.fill")
}
}
}
.alert(isPresented: $showAlert) {
Alert(
title: Text("エラー"),
message: Text("タスク名を入力してください"),
dismissButton: .default(Text("OK"))
)
}
}
}
// 関数宣言する所
private func addItem() {
if taskName.isEmpty {
showAlert = true
} else {
withAnimation {
let newItem = Item(id: UUID(), taskName: taskName, priority: priority, timestamp: Date())
modelContext.insert(newItem)
dismiss()
}
}
}
}
// プレビュー画面
#Preview {
AddTodoView()
.modelContainer(for: Item.self, inMemory: true)
}
コードが長いのでポイントだけ解説していきます
テキストフィールドのどこかをタップすれば入力モード
.onTapGesture {
isTextFieldFocused = true
}
.focused($isTextFieldFocused)
これがないとテキストフィールドの左端を狙ってタップしないといけなくて面倒です
pickerにSegmentedPickerStyle適用
ピッカーのところです
Picker("優先度", selection: $priority) {
ForEach (priorities, id: \.self) {
Text($0)
}
}
.pickerStyle(SegmentedPickerStyle())
.frame(height: 50)
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
.pickerStyle(SegmentedPickerStyle())
以上のところプルダウンではないようにしています
個人的にプルダウン開いて選択するのは手間なので
項目が少ない時はユーザーにとってもこれが良いと思います
ばつボタンを押して前の画面に戻る処理
@Environment(\.dismiss) private var dismiss
~~~ 省略 ~~~
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
Label("Back", systemImage: "xmark.app.fill")
}
}
}
dismissという変数を宣言してボタンのところで実行します
xcodeのバージョンが古い人はコードが異なると思います
ボタンを押してSwiftDataに保存
private func addItem() {
// テキストフィールドが空ならアラート表示
if taskName.isEmpty {
showAlert = true
} else {
withAnimation {
// newItem変数に生成したItemクラスを格納
// taskName => テキストフィールド, priority => picker
let newItem = Item(id: UUID(), taskName: taskName, priority: priority, timestamp: Date())
// 格納したnewItemを保存してdismissでシート閉じる
modelContext.insert(newItem)
dismiss()
}
}
}
データを保存するときの処理が個人的に好きです
modelContextという変数を宣言してinsertするだけ…楽ですね
@Environment(\.modelContext) private var modelContext
~~~ 省略 ~~~
modelContext.insert(newItem)
これでAddTodoView完成です
ContentView作成
ここからはSwiftDataからデータ取得して表示する画面を作っていきます
import SwiftData
import SwiftUI
struct ContentView: View {
@State private var showAddView = false
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
var body: some View {
NavigationStack {
VStack {
if items.isEmpty {
Spacer()
Text("No Data")
.foregroundColor(.gray)
.font(.title)
Spacer()
} else {
List {
ForEach(items.sorted(by: { $0.timestamp < $1.timestamp })) { item in
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(
item.priority == "高" ? .red :
item.priority == "中" ? .yellow :
item.priority == "低" ? .green :
.gray
)
Text(item.taskName)
}
.swipeActions {
Button(role: .destructive) {
deleteItem(item)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
}
}
.navigationTitle("Todo")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showAddView.toggle()
} label: {
Label("Add Task", systemImage: "plus.app")
}
}
}
.sheet(isPresented: $showAddView) {
AddTodoView()
}
}
}
// アイテム削除処理
private func deleteItem(_ item: Item) {
modelContext.delete(item) // contextを使ってItemを削除
try? modelContext.save() // 保存
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
}
ここもポイントだけ解説します
データを並べる
@Query private var items: [Item]
~~~ 省略 ~~~
List {
ForEach(items.sorted(by: { $0.timestamp < $1.timestamp })) { item in
HStack {
Item.swiftで作成したItemクラスをitemsとして宣言します
その後ForEachでitemsを展開しつつtimestamp変数で作成時間で並び替えています
todo項目の重要度によって色を変える
Image(systemName: "checkmark.circle.fill")
.foregroundColor(
item.priority == "高" ? .red :
item.priority == "中" ? .yellow :
item.priority == "低" ? .green :
.gray
)
入力したデータをスワイプで削除する
.swipeActions {
// ボタンの目的の説明を提供。データの削除を実行することを示す
Button(role: .destructive) {
deleteItem(item)
} label: {
Label("Delete", systemImage: "trash")
}
}
HStackに「.swipeActions」を設定します
ボタンロールは、ボタンの目的の説明を提供します。たとえば、destructive ロールは、ボタンがユーザーデータの削除などの破壊的なアクションを実行することを示します。
ボタンを押すとシートを表示する
@State private var showAddView = false
~~~ 省略 ~~~
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showAddView.toggle()
} label: {
Label("Add Task", systemImage: "plus.app")
}
}
}
.sheet(isPresented: $showAddView) {
AddTodoView()
}
ボタンを押すとshowAddview変数とtoggleで反転して
.sheetのところでAddTodoViewを表示
SwiftDataのデータを削除
private func deleteItem(_ item: Item) {
modelContext.delete(item)
// 変更した項目を保存
try? modelContext.save()
}
これでtodoアプリの完成です
完成品の確認
一度実機にビルドしてみましょう
xcodeはすぐに実機ビルドできるので便利です
コードの抜けがあったらすみません…笑
ここから色々アレンジをして面白いアイデアを織り込むのもありかもです
Discussion