3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SwiftData】アプリのデータモデルを永続化する

Last updated at Posted at 2024-06-08

この記事は何?

「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
}

ビュー

スクリーンショット 2024-06-05 1.54.47.png

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は構造体にイニシャライザを自動的に提供するが、クラスには自動的にイニシャライザが提供されない。
したがって、クラスではプログラマーがイニシャライザを作成する。

構造体をSwiftDataモデルに変換する
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クエリで操作できるようになる。
なお、このセクションの手順を完了するまで、アプリはビルドできなくなることを理解しておくこと。

スクリーンショット 2024-06-08 22.16.09.png

まず、コンテナを設定してから、@State配列をSwiftDataクエリに変更する。
そして、アプリが再び動作する前に、新しい友達データを正しく保存できるようにする。

  1. BirthdaysApp.swift を開いて、SwiftDataをインポートする
  2. そして、.modelContainer(for:)モディファイアFriendモデルを指定して、SwiftDataとSwiftUIを接続する

コンテナの役割は、友達データの保存場所とContentView画面を仲介すること。
Friend.selfは特定の友達データではなく、Friend型の定義を指す。
コンテナはモデルとなる型の定義を理解して、モデルデータを正しく保存する。

  • ContentView.swiftで、SwiftDataをインポートする
  • .modelContainerモディファイアで、プレビューにモデルコンテナを追加する
  • .modelContainerモディファイアのinMemory引数にtrueを指定する

プレビューはいつでも、更新するたびに同じ初期状態で開始する必要がある。
inMemorytrueと指定することで、コンテナのストレージシステムがインメモリコンテナ方式になる。
つまり、アプリがメモリ上にある場合にのみ、データが保存される。

  • 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メソッドを「ModelContextinsert(_:)メソッド」に置き換える

ModelContextに挿入すると、新しい友達がコンテナに保存される。
@Queryもコンテナに接続されているので、明示的にnewFriendfriends配列に追加する必要はない。
画面には、新しい友達がピックアップ表示されます。

  • .taskで以前のサンプルデータを作成し、SwiftDataに保存する

SwiftUIはビューが表示される直前に、.taskのコードを実行する。
したがって、モデルオブジェクトを挿入すると、@Queryがそれらをピックアップしてfriendsプロパティを更新する。

Experiment
シミュレータでアプリを実行([Product > Run]を選択)します。その後、アプリをもう一度実行してください。プレビューと比較して、シミュレーターにはたくさんの友達がいる理由を考察してみましょう。

SwiftDataとSwiftUIの接続
/* 
 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)
}

参考

Date
NavigationStack
SwiftData
@Query
EnvironmentValues

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?