この記事は何?
「Develop in Swift Tutorials」のModels and persistenceを解説します。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。
アプリのデータを保存するには
友達の誕生日を表示するアプリを構築して、データモデリングを学ぶ。
まず、データモデルを作成する。
そして、SwiftDataフレームワークと統合して、アプリ起動時にデータがリセットされないようにする。
Birthdaysアプリケーションのコード
データモデル
新しいiOSプロジェクトを作成する
StorageオプションはNone
Friend.swiftファイルを作成して、Friend構造体を定義する
import Foundation
struct Friend {
let name: String
let birthday: Date
}
ビュー
import SwiftUI
struct ContentView: View {
@State private var friends: [Friend] = [
Friend(name: "Elton Lin", birthday: .now),
Friend(name: "Jenny Court", birthday: Date(timeIntervalSince1970: 0))
]
@State private var newName = ""
@State private var newDate = Date.now
var body: some View {
NavigationStack {
List(friends, id: \.name) { friend in
HStack {
Text(friend.name)
Spacer()
Text(friend.birthday, format: .dateTime.month(.wide).day().year())
}
}
.navigationTitle("Birthdays")
.safeAreaInset(edge: .bottom) {
VStack(alignment: .center, spacing: 20) {
Text("New Birthday")
.font(.headline)
DatePicker(selection: $newDate, in: Date.distantPast...Date.now, displayedComponents: .date) {
TextField("Name", text: $newName)
.textFieldStyle(.roundedBorder)
}
Button("Save") {
let newFriend = Friend(name: newName, birthday: newDate)
friends.append(newFriend)
newName = ""
newDate = .now }
.bold()
}
.padding()
.background(.bar)
}
}
}
}
構造体をSwiftDataモデルに変換する
誕生日を思い出すためのアプリは、入力したデータを保存できなければ役に立たない。
ユーザーが作成したFriend
型インスタンスをSwiftDataフレームワークで保存し、アプリを再起動してもデータが消えないようにする。
- Friend.swift ファイルを開き、インポートしてSwiftDataにアクセスする
SwiftDataはアプリ内のデータをモデル化して、永続性のあるデータを保存できる。
そのために、ユーザーがアプリから離れてもデータが消えないようにするツールを提供する。
-
Friend
構造体に@Model
マクロをマークする
@Model
マクロに使用するには、SwiftDataフレームワークが必要。
@Model
は、SwiftクラスをSwiftDataによって管理される格納モデルに変換するための、隠しコードを生成する。
Note
マクロが生成した隠しコードによって引き起こされるエラーは、以降のステップで解消します。
[Product > Build]を選択して、プロジェクトをビルドする。
ここで発生するエラーは、「Friend
型の@Model
を、構造体ではなくクラスに適用すべきある」ことを示している。
-
Friend
型をクラスにする
構造体とは異なり、クラスのインスタンスにはアイデンティティの概念がある。
SwiftDataはアイデンティティを使用して、アプリ全体にわたってモデルデータを適切なビューと共有する。
いずれかのビューがモデルを変更すると、SwiftDataはそれをすぐに見つけます。
Note
イシューナビゲーターには、ビルド時に発生したエラーが表示されます。エラーを解決してから、プロジェクトナビゲーターに切り替えてください。
-
Friend
型にイニシャライザを追加する - そして、プロジェクトを再度ビルドして、エラーが解決されたことを確認する
イニシャライザは、すべてのプロパティを初期化することで、型のインスタンスを作成する。
Swiftは構造体にイニシャライザを自動的に提供するが、クラスには自動的にイニシャライザが提供されない。
したがって、クラスではプログラマーがイニシャライザを作成する。
import Foundation
import SwiftData
@Model
class Friend {
let name: String
let birthday: Date
init(name: String, birthday: Date) {
self.name = name
self.birthday = birthday
}
}
SwiftDataとSwiftUIの接続
SwiftDataモデルにアップグレードすると、SwiftUIの@State
配列をSwiftDataクエリで操作できるようになる。
なお、このセクションの手順を完了するまで、アプリはビルドできなくなることを理解しておくこと。
まず、コンテナを設定してから、@State
配列をSwiftDataクエリに変更する。
そして、アプリが再び動作する前に、新しい友達データを正しく保存できるようにする。
- BirthdaysApp.swift を開いて、SwiftDataをインポートする
- そして、
.modelContainer(for:)
モディファイアにFriend
モデルを指定して、SwiftDataとSwiftUIを接続する
コンテナの役割は、友達データの保存場所とContentView
画面を仲介すること。
Friend.self
は特定の友達データではなく、Friend
型の定義を指す。
コンテナはモデルとなる型の定義を理解して、モデルデータを正しく保存する。
- ContentView.swiftで、SwiftDataをインポートする
-
.modelContainer
モディファイアで、プレビューにモデルコンテナを追加する -
.modelContainer
モディファイアのinMemory
引数にtrue
を指定する
プレビューはいつでも、更新するたびに同じ初期状態で開始する必要がある。
inMemory
をtrue
と指定することで、コンテナのストレージシステムがインメモリコンテナ方式になる。
つまり、アプリがメモリ上にある場合にのみ、データが保存される。
-
friends
配列の属性を@State
から@Query
に変更する
そうすることで、SwiftDataによって管理されているFriend
型インスタンスを取得できるようになる。
@Query
属性のプロパティは、SwiftDataに配列データ(この場合は[Friend]
)を要求する。
SwiftDataによって管理されるFriend
型インスタンスを更新すると、(@State
プロパティと同様に)クエリがビューを更新する。
Note
SwiftDataのサンプルデータは、クエリで直に作成できません。このセクションの最後でエラーを解決したら、サンプルデータを元に戻します。
- SwiftDataで新しいアイテムを保存するには
ModelContext
が必要なので、環境値の\.modelContext
にアクセスするcontext
プロパティを宣言する
ModelContext
は「ビューとモデルコンテナの接続」を提供し、コンテナ内のアイテムを取得、挿入、削除できるようにする。
ContentView
の.modelContainer
モディファイアは、SwiftUI環境にmodelContext
を挿入する。
そのおかげで、modelContext
は「コンテナより下層のすべてのビュー」にアクセスできる。
- 「Saveボタン」のコードで、新しい友達インスタンスを追加する
append
メソッドを「ModelContext
のinsert(_:)メソッド」に置き換える
ModelContext
に挿入すると、新しい友達がコンテナに保存される。
@Query
もコンテナに接続されているので、明示的にnewFriend
をfriends
配列に追加する必要はない。
画面には、新しい友達がピックアップ表示されます。
-
.task
で以前のサンプルデータを作成し、SwiftDataに保存する
SwiftUIはビューが表示される直前に、.task
のコードを実行する。
したがって、モデルオブジェクトを挿入すると、@Query
がそれらをピックアップしてfriends
プロパティを更新する。
Experiment
シミュレータでアプリを実行([Product > Run]を選択)します。その後、アプリをもう一度実行してください。プレビューと比較して、シミュレーターにはたくさんの友達がいる理由を考察してみましょう。
/*
BirthdaysApp.swift
*/
import SwiftUI
import SwiftData
@main
struct BirthdaysApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Friend.self)
}
}
}
/*
ContentView.swift
*/
import SwiftUI
import SwiftData
struct ContentView: View {
@Query private var friends: [Friend]
@Environment(\.modelContext) private var context
@State private var newName = ""
@State private var newDate = Date.now
var body: some View {
NavigationStack {
List(friends, id: \.name) { friend in
HStack {
Text(friend.name)
Spacer()
Text(friend.birthday, format: .dateTime.month(.wide).day().year())
}
}
.navigationTitle("Birthdays")
.safeAreaInset(edge: .bottom) {
VStack(alignment: .center, spacing: 20) {
Text("New Birthday")
.font(.headline)
DatePicker(selection: $newDate, in: Date.distantPast...Date.now, displayedComponents: .date) {
TextField("Name", text: $newName)
.textFieldStyle(.roundedBorder)
}
Button("Save") {
let newFriend = Friend(name: newName, birthday: newDate)
context.insert(newFriend)
newName = ""
newDate = .now }
.bold()
}
.padding()
.background(.bar)
}
.task {
context.insert(Friend(name: "Elton Lin", birthday: .now))
context.insert(Friend(name: "Jenny Court", birthday: Date(timeIntervalSince1970: 0)))
}
}
}
}
#Preview {
ContentView()
.modelContainer(for: Friend.self, inMemory: true)
}
Use model data to fill out the UI
友達を年齢順にソートして、今日が誕生日の友達を強調し、日付を人間が読みやい表示にします。
SwiftDataはモデルの各インスタンスに対して、データとは別に独自のアイデンティティを提供します。
-
@Model
が提供するアイデンティティを使用するために、リストの明示的なid指定を削除する
今まではリスト内の各Friend
インスタンスを識別するためにname
プロパティを使用していたが、SwiftDataによってインスタンスを識別できるようになった。
もはや、同じ名前の友達が何人いても問題ない。
誕生日を使って、リストの友達を時系列で並べ替える。
前年の生年月日は月や日に関係なく、後年の誕生日より先の要素になる。
Experiment
別の人を、エルトンとジェニーの間に追加してみましょう。
-
Friend
型に「友達の誕生日が今日か」を算出するための、計算プロパティを追加する
Calendar
型は、絶対時点(日付)を年、月、分、秒などの単位に変換する。
世界にはさまざまなカレンダーシステムがあるが、Calendar.current
は「アプリを実行しているデバイス」のカレンダー設定にアクセスする。
- ContentView.swift を開く
-
isBirthdayToday
メソッドを使用して、「誕生日が今日の友人」について名前をボールドにする - さらに、ケーキのSFシンボルで強調する。
Experiment
ケーキのかわりになりそうな、パーティー関連のSFシンボルを探しましょう。
- アプリのデータを事前に入力する
.task
を削除しておく。
Experiment
シミュレーターでアプリを起動して、アプリの全機能を試してください。
友達を追加し、誕生日の順にリスト表示されることを確認しましょう。
/*
Friend.swift
*/
import Foundation
import SwiftData
@Model
class Friend {
let name: String
let birthday: Date
var isBirthdayToday: Bool {
Calendar.current.isDateInToday(birthday)
}
init(name: String, birthday: Date) {
self.name = name
self.birthday = birthday
}
}
/*
ContentView.swift
*/
import SwiftUI
import SwiftData
struct ContentView: View {
@Query(sort: \Friend.birthday) private var friends: [Friend]
@Environment(\.modelContext) private var context
@State private var newName = ""
@State private var newDate = Date.now
var body: some View {
NavigationStack {
List(friends) { friend in
HStack {
if friend.isBirthdayToday {
Image(systemName: "birthday.cake")
}
Text(friend.name)
.bold(friend.isBirthdayToday)
Spacer()
Text(friend.birthday, format: .dateTime.month(.wide).day().year())
}
}
.navigationTitle("Birthdays")
.safeAreaInset(edge: .bottom) {
VStack(alignment: .center, spacing: 20) {
Text("New Birthday")
.font(.headline)
DatePicker(selection: $newDate, in: Date.distantPast...Date.now, displayedComponents: .date) {
TextField("Name", text: $newName)
.textFieldStyle(.roundedBorder)
}
Button("Save") {
let newFriend = Friend(name: newName, birthday: newDate)
context.insert(newFriend)
newName = ""
newDate = .now }
.bold()
}
.padding()
.background(.bar)
}
}
}
}
#Preview {
ContentView()
.modelContainer(for: Friend.self, inMemory: true)
}