フューチャー技術ブログ

Develop in SwiftでSwiftDataの基本を学ぶ ~Models and persistence編~

はじめに

HealthCare Innovation Group(HIG)1の橋本です。

新しく登場した公式チュートリアルDevelop in SwiftのModels and persistence、Data editing and navigation、Relationships and queriesという3つのセクションでSwiftDataを学べるとのことで実際にやってみました。

今回は、1つ目の Models and persistence編 です。Models and persistenceセクションで学んだこと、Wrap-upのExtend your appの追加課題をやってみたので、これらについてまとめています。

本記事でわかること

  • SwiftDataの基本的な使い方
    • SwiftDataの導入
    • @Model, @Queryの使い方
    • SwiftUIViewとの連携
  • Wrap-upのExtend your appの追加課題の解答例を知ることができる

環境

  • OS: macOS Sonoma 14.4.1
  • Xcode: 15.3 (15E204a)
  • Swift: 5.10

目次

  • SwiftDataとは
  • Save data
    • Section 1~3(UI等のSwiftDataに直接関係のない事前準備)
    • Section 4: Convert your structure to a SwiftData model
    • Section 5: Connect SwiftData and SwiftUI
    • Section 6: Use model data to fill out the UI
  • Wrap-up: Models and persistence
  • おわりに

SwiftDataとは

SwiftDataとは、データモデリングとデータの永続化のフレームワークです。
これまで主に使われていたCoreDataの後継として期待されています。
SwiftDataの主な特徴としては、

  • SwiftUIとの連携
    • SwiftUIと深く統合されており、ユーザーインタフェースのためのデータバインディングが非常にスムーズ
  • 宣言的データモデリング:
    • 属性、関係性、オブジェクトのバリデーションなどを宣言的に定義できること
  • データ永続化:
    • オブジェクトが自動的に保存され、データ永続化が容易であること
  • パフォーマンス最適化:
    • Appleのエコシステムに合わせて最適化されており、効率的なクエリ実行が可能

注意
SwiftDataはiOS17.0+, iPadOS17.0+で使用可能であること。

Save data

Models and persistenceセクションのSave dataを進めていきます。

Section 1~3(UI等のSwiftDataに直接関係のない事前準備)

以下は、Section1~3まで対応後のコードです。この時点ではSwiftDataを使用していないため、誕生日を登録しても、アプリキルすると登録したデータは削除されてしまいます。

BirthdayApp.swift
import SwiftUI

@main
struct BirthdaysApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Friend.swift
import Foundation

struct Friend {
let name: String
let birthday: Date
}
ContentView.swift
import SwiftUI

struct ContentView: View {
@State private var friends: [Friend] = [
Friend(name: "Elton", 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)
}
}
}
}

Section4: Convert your structure to a SwiftData model

先程作成したFriend構造体をSwiftDataモデルに変換します。

以下、4点を対応します。

  • SwiftDataフレームワークをインポートします
  • モデルに対して、@Modelマクロのアノテーションを付与します
  • structからclassに書き換えます
  • classstructと異なり、自動でイニシャライザが生成されないため、イニシャライザを用意します

Friend.swift

import Foundation
+ import SwiftData

+ @Model
- struct Friend {
+ class Frined {
let name: String
let birthday: Date

+ init(name: String, birthday: Date) {
+ self.name = name
+ self.birthday = birthday
}

Section5: Connect SwiftData and SwiftUI

SwiftDataSwiftUIViewを連携させます。
SwiftUIにおけるエントリーポイントである~App.swiftに次のコードを追加することで、SwiftDataによって永続化させたデータの保存場所とViewを連携させることができます。

BirthdaysApp.swift

import SwiftUI
+ import SwiftData

@main
struct BirthdaysApp: App {
var body: some Scene {
WindowGroup {
ContentView()
+ .modelContainer(for: Friend.self)
}
}
}

ContentView.swift

  • friends配列を@Stateから@Queryに変更することで、@Queryマクロによって、モデルに変更があったときにSwiftUIViewに自動的に変更を伝えることができます。
  • modelContextを環境変数として宣言します。
    • SwiftDataにおいて、ModelContextによってビューとモデルコンテナ間を接続し、データの取得、挿入、削除が可能になります。
import SwiftUI
+ import SwiftData

struct ContentView: View {
+ @Query private var friends: [Friend]
- @State private var friends: [Friend] = [
- Friend(name: "Elton", birthday: .now),
- Friend(name: "Jenny Court", birthday: Date(timeIntervalSince1970: 0))
- ]
+ @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)
- friends.append(newFriend)
newName = ""
newDate = .now
}
.bold()
}
.padding()
.background(.bar)
}
}
}
}

Section6: Use model data to fill out the UI

モデルデータを使ってUIをいい感じに整えていきます。
主に以下2点を実現させます。

  • 登録されているデータを日付順でソートする。
  • 誕生日当日の人は、ケーキのマークがつくようにする。

BirthDayApp.swift
Section5から追加の修正なし。

Friend.swift

今日が誕生日かどうかを表すために、コンピューテッドプロパティとして、Bool型のisBirthdayTodayを用意します。

import Foundation
import SwiftData

@Model
class Frined {
let name: String
let birthday: Date

init(name: String, birthday: Date) {
self.name = name
self.birthday = birthday

+ var isBirthdayToday: Bool {
+ Calendar.current.isDateInToday(birthday)
+ }
}

ContentView.swift

  • 追加された人の誕生日を降順で並ぶように、@Query@Query(sort: \Friend.birthday)に修正します。@Query だけの場合は、いつも同じ並び順にはならないことに注意してください。
  • SwiftDataは各モデルインスタンスに独自のIDを提供します。Listでは、@Model が提供する識別子を使うため、明示的なIDを削除します。つまり、List()の引数であるKeyPathを削除します。
import SwiftUI
import SwiftData

struct ContentView: View {
+ @Query(sort: \Friend.birthday) private var friends: [Friend]
- @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) { friend in
- 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)
}
}
}
}

Wrap-up: Models and persistence

次のページのExtend your appの次の2つのお題に取り組みたいと思います。

https://developer.apple.com/tutorials/develop-in-swift/models-and-persistence-conclusion

Extend your app(ソートする基準の変更、降順or昇順)

Sort the birthday list by name instead of birthday.

これはとても簡単に修正できます。
@Query(sort: \Friend.birthday)@Query(sort: \Friend.name)に変えるだけです。

@Query(sort: \Friend.name) private var friends: [Friend]

これで、誕生日順ではなく、名前の降順にできました。これを昇順にする場合は、引数order: .reverseを与えるだけで実現できます。

@Query(sort: \Friend.name, order: .reverse) private var friends: [Friend]

Add a notes property to Friend to plan how you’ll celebrate a friend’s birthday.

これもおまけ的な内容ですが、簡単に実装してみます。

Friend.swift
import Foundation
import SwiftData

@Model
class Frined {
let name: String
let birthday: Date
+ let notes: String

+ init(name: String, birthday: Date, notes: String) {
- init(name: String, birthday: Date) {
self.name = name
self.birthday = birthday
+ self.notes = notes

var isBirthdayToday: Bool {
Calendar.current.isDateInToday(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
+ @State private var newNotes = ""

var body: some View {
NavigationStack {
List(friends) { friend in
HStack {
if friend.isBirthdayToday {
Image(systemName: "birthday.cake")
}
VStack {
Text(friend.name)
.bold(friend.isBirthdayToday)
+ Text(friend.notes)
+ .font(.caption2)
}
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)
}
+ TextField("Notes", text: $newNotes)
+ .textFieldStyle(.roundedBorder)
Button("Save") {
let newFriend = Friend(name: newName, birthday: newDate, notes: newNotes)
context.insert(newFriend)
newName = ""
newDate = .now
+ newNotes = ""
}
.bold()
}
.padding()
.background(.bar)
}
}
}
}

完成したサンプルアプリ

追加した友達の誕生日のデータがアプリをキルしても、再度立ち上げると残っていることが確認できました!
SwiftDataを使って、データを永続化させることに成功!!

おわりに

公式チュートリアルDevelop in Swiftを使ってSwiftDataの基本的な使い方を学びました。気になった方は、実際にチュートリアルを1つずつ実際にコードを書きながら、進めることをおすすめします。

次回は、公式チュートリアルDevelop in SwiftのData editing and navigation編を公開したいと思います。

参考


  1. 1.医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは次の記事をご覧ください。”新規事業の立ち上げ フューチャーの知られざる医療・ヘルスケアへの挑戦”