iOS・AndroidSwift

SwiftData入門

iOS・Android
この記事は約15分で読めます。

SwiftDataは、WWDC2023で発表されたばかりの最新のデータ永続化フレームワークです。

Xcode の新規プロジェクト作成時に生成されるテンプレートコードは、非常にシンプルな作りとなっており、SwiftDataへの一歩を踏み出す上で最適な教材であると考えています。

本稿では、現状Apple公式が準備しているドキュメントを踏まえ、適宜CoreDataと比較を行いつつ、テンプレートの解説を行います。

SwiftData概要

SwiftDataはデータ永続化フレームワークです。iOS17以降で使用することができます。

最小限のコードでアプリのモデルレイヤーを記述することができます。iCloudを通じて複数デバイス間でのデータ同期を実現します。

SwiftUIとの親和性が高く、少ないコードでデータの永続化を行うことができます。

SwiftData | Apple Developer Documentation
Write your model code declaratively to add managed persistence and automatic iCloud sync.

Xcode の新規プロジェクトで保存されるテンプレートを見ることで、最小限の構成を把握することができます。

こちらを参照し、まずは主要な要素の把握を行いましょう。

SwiftDataSumpleApp.swift

このファイルは、アプリのエントリポイントになっています。

ここでは、アプリ内で共通して使用されるデータモデルの型や設定を行い、それをアプリ全体に適応しデータベース操作を行う環境の準備を行っています。

import SwiftUI
import SwiftData

@main
struct SwiftDataSumpleApp: 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)
    }
}

ここでは、SchemaItemを設定しています。

アプリのエントリポイントでスキーマを定義することにより、アプリケーションがビルドされるときに最新のデータ構造に基づいてデータストアが設定されます。これにより、モデルクラスとデータベースの間の構造的な不整合を防ぎ、データの不整合が発生することを防いでいます。

let schema = Schema([
            Item.self,
        ])
Schema | Apple Developer Documentation
An object that maps model classes to data in the model store, and helps with the migration of that data between releases.

ModelContainerSchemaModelConfiguration から生成されます。

modelConfigurationにて、スキーマとストレージオプションを指定し、データモデルの設定を行っています。ここでは、永続化ストアにデータを保存する設定を行っています。

ModelContainerを通じて、データの保存、取得、変更などを行います。
データモデルに関連する操作を行う作業領域を示す概念なので、CoreDataでいうところのNSPersistentContainerに近い概念なのかなという印象を受けています。

sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self,
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        }
ModelConfiguration | Apple Developer Documentation
A type that describes the configuration of an app’s schema or specific group of models.
ModelContainer | Apple Developer Documentation
An object that manages an app’s schema and model storage configuration.

.modelContainer修飾子を使用することで、シーンの環境にモデルコンテナと関連するモデルコンテキストを設定することができます。

アプリケーション内のすべてのビューは、この共有コンテナを介してデータ操作を行うことができ、一貫したデータ管理を実現します。

CoreDataでいうところのNSManagedObjectContextに近い印象です。

WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
modelContainer(_:) | Apple Developer Documentation
Sets the model container and associated model context in this scene’s environment.

ここまで見てきたコードは、以下のコードと機能としては同一です。

import SwiftUI
import SwiftData

@main
struct SDSample01App: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Item.self)
    }
}

上記のコードでは、.modelContainer の初期化時にModelContainerを渡していました。

func modelContainer(_ container: ModelContainer) -> some Scene
modelContainer(_:) | Apple Developer Documentation
Sets the model container and associated model context in this scene’s environment.

.modelContainer の初期化手法は2つあります。
サンプルコードが使用していない、永続化対象のモデル構造を渡し、よしなにコンテナを作成してもらう簡易的な手法も提供を活用すると、ここまでのコードを相当簡略化することができます。

func modelContainer(
    for modelType: PersistentModel.Type,
    inMemory: Bool = false,
    isAutosaveEnabled: Bool = true,
    isUndoEnabled: Bool = false,
    onSetup: @escaping (Result<ModelContainer, Error>) -> Void = { _ in }
) -> some Scene

inMemoryはもともとfalseになっており、デフォルト設定でデータをメモリ内にのみ保存する設定になっています。

modelContainer(for:inMemory:isAutosaveEnabled:isUndoEnabled:onSetup:) | Apple Developer Documentation
Sets the model container in this scene for storing the provided model types, creating a new container if necessary, and also sets a model context for that conta...

Item.swift

ここではデータ構造の定義を行なっています。

@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

データベースのエンティティを設定しています。

プロパティは Date 型の timestamp のみです。
先ほど見たコードの let schema = Schema([Item.self,])Itemはこれを示しています。

CoreDataと比較し、非常に簡潔にデータモデルの定義が行えるようになっていますね。

@Modelマクロは、SwiftクラスをSwiftDataによって管理される保存されたモデルに変換します。

※ マクロ: コンパイル時に定型的に繰り返されるコードを生成する仕組み。

Model() | Apple Developer Documentation
Converts a Swift class into a stored model that’s managed by SwiftData.

ContentView.swift

UIの見た目と簡単なデータ操作を設定しています。

ここでは、SwiftDataに関連する記述に注目します。

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]

    var body: some View {
        NavigationSplitView {
            List {
                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)
}

@Environment(\.modelContext) private var modelContext の  .modelContext は @Environment 用の新しい Environment値です。
この宣言を通じ、modelContext 変数に、この環境内のクエリやその他のモデル操作に使用されるSwiftDataモデルコンテキストを設定できます。

modelContext | Apple Developer Documentation
The SwiftData model context that will be used for queries and other model operations within this environment.

@Query private var items: [Item] により、設定されたコンテキスト内でモデルの変更が行われた際、自動的にViewの再描画の実行が果たされます。
まだ公式のドキュメントに詳細が記載されていないのですが、かなり強力な機能という印象を受けています。

Query | Apple Developer Documentation
A property wrapper that fetches a set of models and keeps those models in sync with the underlying data.

データの追加を行う関数がaddItem()です。

private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem)
        }
    }

先ほど@Environmentを介して取得したmodelContextinsert(_:)を用いて、newItemを追加しています。

insert(_:) | Apple Developer Documentation
There's never been a better time to develop for Apple platforms.

データの削除を行う関数がdeleteItems()です。

private func deleteItems(offsets: IndexSet) {
    withAnimation {
        for index in offsets {
            modelContext.delete(items[index])
        }
    }
}

CoreDataを使用する際は、データの削除を行う際はhasChangeを確認した上でfunc save() を行いましたが、SwiftDataの際は .delete のみで実行できるようです。

delete(_:) | Apple Developer Documentation
There's never been a better time to develop for Apple platforms.

SwiftData はデータの永続化が目的ですが、プレビューを表示する部分では、Viewの内部でのみコンテナのためのモデルコンテキストを設定しています。

この設定により、プレビューでも追加削除を試すことができます。
データを保存しないので、アプリを起動する都度まっさらな状態で表示されます。
地味に便利な機能ですね。

#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true)
}
modelContainer(for:inMemory:isAutosaveEnabled:isUndoEnabled:onSetup:) | Apple Developer Documentation
Sets the model container in this view for storing the provided model type, creating a new container if necessary, and also sets a model context for that contain...

公式のサンプルコード

次のステップに良さそうな公式のドキュメントをまとめました。

現状4つサンプルプロジェクトが提供されています。

Building a document-based app using SwiftData | Apple Developer Documentation
Code along with the WWDC presenter to transform an app with SwiftData.
Adopting SwiftData for a Core Data app | Apple Developer Documentation
Persist data in your app intuitively with the Swift native persistence framework.
  • 読み取り専用のネットワークデータをキャッシュするための永続ストアを作成および更新するサンプル。
  • 一日分の地震のリストを表示し、各地震の発生時間、場所、規模を示す
  • Bring Core Data concurrency to Swift and SwiftUI」セッションと対応
Maintaining a local copy of server data | Apple Developer Documentation
Create and update a persistent store to cache read-only network data.
  • SwiftDataによって管理されるデータを収集および変更するためのデータ入力フォームを作成するサンプルアプリ
Adding and editing persistent data in your app | Apple Developer Documentation
Create a data entry form for collecting and changing data managed by SwiftData.

まとめ

本稿では、SwiftDataの概要と、テンプレートコードの説明を行いました。

全体的に、CoreDataと比較し相当コードの記述量を削減できるなという印象を受けています。

@Model@Query の機能の強力さも非常に感じました。

一方で、まだまだドキュメントが充実しておらず、詳細な内部挙動を把握するための試行錯誤が必要な印象も受けています。

この記事が、SwiftDataを把握する際の一助となれば幸いです。

ここまで読んでくださりありがとうございました。