この記事は何?
XcodeのiOSプロジェクトについて、ストレージ設定で「SwiftData」を選択した際のテンプレートコードを解説する。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。
実行環境
- Swift 6
- macOS Sonoma 14.5
- Xcode 15.3
テンプレートのコード
プロジェクトのストレージ設定でSwiftDataを選択すると、ナビゲーション構造を持ったリスト表示のアプリが構築される。
この時点で、リストの行データは追加と削除が可能。
下の画像は、リストに5つの行データを追加して、4つ目の行を削除しようとしているところ。
以下では、SwiftData向けのコードが追加されている3つのファイルを解説する。
SwiftDataTemplateApp.swift
モデルコンテナ を作成して、アプリ全域で共有できるようにSwiftUIビューと接続する。
モデルコンテナは「SwiftDataモデルの倉庫」として機能する。
import SwiftUI
import SwiftData // フレームワークを導入
@main
struct SwiftDataTemplateApp: App {
// アプリ全域で使用するためのモデルコンテナを作成する
var sharedModelContainer: ModelContainer = {
// スキーマとモデルコンフィグを作成する
let schema = Schema([Item.self,])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
// モデルコンテナを作成できなければ、致命的なエラーでアプリを終了
do {
// スキーマとモデルコンフィグを指定して、モデルコンテナを初期化して返す。
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer) // アプリのビュー階層トップで、作成したモデルコンテナを接続
}
}
ContentView.swift
リスト形式でデータを表示する、アプリの基本画面。
ビューは「環境値のモデルコンテキスト」を介して、永続ストレージにアクセスしている。
@Query
プロパティラッパーがマークされたクエリ変数のitems
プロパティはSwiftUIによって追跡されているので、変更が発生するとビューは再描画される。
import SwiftUI
import SwiftData // フレームワークを導入
struct ContentView: View {
@Environment(\.modelContext) private var modelContext // ビュー環境からモデルコンテキストにアクセス
@Query private var items: [Item] // SwiftDataモデルからデータをフェッチするクエリ
var body: some View {
NavigationSplitView {
List {
// 行コンテンツのクロージャは、クエリ変数のitems配列にアクセスする
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
} detail: {
Text("Select an item")
}
}
// モデルコンテキストにデータを挿入する
private func addItem() {
withAnimation {
let newItem = Item(timestamp: Date())
modelContext.insert(newItem)
}
}
// モデルコンテキストのデータを削除する
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
}
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true) // プレビュー向けのモデルコンテナを指定
}
ここで、プレビューに適用したモデルコンテナは、「SwiftDataTemplateApp.swift で作成したモデルコンテナ」とは別物である点に注意すること。
SwiftDataTemplateApp.swift では、.modelContainer(_:)
モディファイアを使用した。
ContentView.swift のプレビューには.modelContainer(for:inMemory:)
モディファイアを使用している。
Item.swift
Item
クラスは「SwiftDataモデルのオブジェクト」を定義している。
クラスに@Model
マクロをマークすると、PersistentModel
プロトコルの適合性が追加されてオブジェクトを永続化できる。
import Foundation
import SwiftData // フレームワーク
// @Modelマクロをマーク
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
プレビュー向けのサンプルデータを永続化する
ContentViewでは、プレビューを操作して行データを追加できる。
ただし、プレビューがリロードされると、追加したアイテムは消えてしまう。
これは、プレビューのモデルコンテナに永続性がないから、リロードされるたびにモデルコンテナが空のデータセットを作成してしまう。
「本番アプリ向けの永続的なデータストア」に干渉しない、「サンプルの永続データ」を作成すれば解決できる。
すべてのプレビューにこのデータを使用させて、データセットの変更を確認できるようにする。
サンプルデータのクラス
- 新しいSwiftファイル「 SampleData.swift 」を新規作成して、
SampleData
クラスを定義する - ファイルにSwiftDataフレームワークをインポートする
import Foundation
import SwiftData // フレームワークの導入
class SampleData {
}
サンプルデータのモデルコンテナ
ビューのモディファイアを使用せずに、モデルコンテナを明示的に構築する。
そうすれば、サンプルデータを1か所で作成して管理し、すべてのプレビューで使用できる。
- モデルコンテナを保持する「定数の
modelContainer
プロパティ」を作成する
import Foundation
import SwiftData // フレームワークの導入
class SampleData { // error; Class 'SampleData' has no initializers
let modelContainer: ModelContainer
}
このモデルコンテナはデータを永続化せずに、メモリに保存させたい。
そのためには、クラスのイニシャライザでSwiftDataに必要なすべてのセットアップを実行する。
例えば、ディスクに保存することなく、データをメモリにのみ保存するようにモデル構成を設定する
- クラスのデフォルトイニシャライザを実装する
class SampleData {
let modelContainer: ModelContainer
init() {
// スキーマとモデル構成を作成
let schema = Schema([Item.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
// モデルコンテナを初期化
self.modelContainer = try! ModelContainer(for: schema, configurations: [modelConfiguration])
}
}
コンテナへのアクセスを管理するコード
SampleDataのインスタンスをSampleDataクラス自体からのみ作成できるようにする。
これによって、shared
が作成するインスタンスは唯一無二であることを保証される。
これは、グローバルな共有オブジェクトを作成するシングルトンパターン。
-
shared
共有インスタンスを作成する - イニシャライザに
private
をマークする
class SampleData {
static let shared = SampleData() // シングルトンのインスタンス
let modelContainer: ModelContainer
private init() { // 外部からのインスタンス作成を禁止
let schema = Schema([Item.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
self.modelContainer = try! ModelContainer(for: schema, configurations: [modelConfiguration])
}
}
コンテキストを作成する
ビューからのSwiftDataモデル操作は、モデルコンテキストを介して行われる。
コードをより簡潔にするため、モデルコンテナのメインコンテキストにアクセスするための計算プロパティを作成する。
@MainActor
class SampleData {
static let shared = SampleData()
let modelContainer: ModelContainer
var context: ModelContext {
return modelContainer.mainContext // モデルコンテナはコンテキストを提供する
}
private init() {
let schema = Schema([Item.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
}
}
Xcodeは「メインアクターからModelContainer
のmainContext
プロパティにアクセスすべき」というエラーを報告する。
このエラーは、複数のタスクを同時に実行する方法であるSwiftの並行性に関連している。
Swiftは、アクターを使用して同時に実行されるコードを管理する。
SampleDataクラスに@MainActor
をマークすると、エラーを解消できる。
これによって、mainContext
プロパティへのアクセスを含めた、このクラスのコードがメインアクターで実行される。
なお、SwiftUIコードは原則、メインアクターで実行される。
@MainActor // メインアクターでデータレースを回避
class SampleData {
static let shared = SampleData()
let modelContainer: ModelContainer
var context: ModelContext {
return modelContainer.mainContext
}
private init() {
// スキーマとモデル構成を作成
let schema = Schema([Item.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
}
}
サンプルの行データインスタンス
SwiftDataモデルで、適当なサンプルデータの配列を型プロパティとして定義する。
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
static let samples = [
Item(timestamp: Date(timeIntervalSinceReferenceDate: -402_000_000)),
Item(timestamp: Date(timeIntervalSinceReferenceDate: 300_000_000)),
Item(timestamp: Date(timeIntervalSinceReferenceDate: -700_000_000)),
]
}
サンプルデータを提供するオブジェクトのイニシャライザで、コンテキストにデータの挿入を実行する。
@MainActor
class SampleData {
static let shared = SampleData()
let modelContainer: ModelContainer
var context: ModelContext {
return modelContainer.mainContext
}
private init() {
let schema = Schema([Item.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
self.modelContainer = try! ModelContainer(for: schema, configurations: [modelConfiguration])
insertSampleData // 挿入メソッドの呼び出し
}
// コンテキストがSwiftDataモデルにデータを挿入する
func insertSampleData() {
for item in Item.samples {
context.insert(item)
}
try! context.save()
}
}
プレビューに永続化されたサンプルデータを接続する
ContentViewのプレビューにモデルコンテナを接続する。
そのためには、モディファイアを変更して「シングルトンのサンプルデータオブジェクト」を指定する。
#Preview {
ContentView()
.modelContainer(SampleData.shared.modelContainer)
}
プレビューがリロードされても、リストにはサンプルの行コンテンツが配置されるようになった。
SampleDataの全体
import Foundation
import SwiftData
@MainActor
class SampleData {
static let shared = SampleData()
let modelContainer: ModelContainer
var context: ModelContext {
return modelContainer.mainContext
}
private init() {
let schema = Schema([Item.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
self.modelContainer = try! ModelContainer(for: schema, configurations: [modelConfiguration])
insertSampleData()
}
func insertSampleData() {
for item in Item.samples {
context.insert(item)
}
try! context.save()
}
}