10
2

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の痒いところに手が届くTips

この記事はand factory.inc Advent Calendar 2025 23日目の記事です。
昨日は @twumo さんの Material3 に移行したら背景色が変わった? tonalElevation の話 でした :christmas_tree:

はじめに

先日、個人開発プロダクトでSwiftDataを導入してみました。

SwiftDataはiOS17以降で使用可能なデータ永続化フレームワークです。

SQL文無しでデータの読み書きができ、構造体型もカラムに定義したりiCloud同期も標準搭載、これだけ機能がてんこ盛りなのにアプリ本体の容量はほとんど増えない(※)という素晴らしいフレームワークです。
(※今回リリースしたアプリのサイズはFirebaseなど他のライブラリ込みで僅か8MBでした)

ただ若干クセはあるっぽく、実装している上で色々躓きポイントがあったのでそれを書き残しておきます。

私が時間を溶かして得た知見なので有料級の情報です。

ModelContainer定義はマイグレーション想定で

SwiftDataの基本の使い方紹介の記事によく↓のようなサンプルがあると思います。

import SwiftData

@main
struct SomeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Item.self)
    }
}

サンプルとしては分かりやすい例ですが、いざ開発を進めてリリースし、しばらく経ってデータテーブルをマイグレーションしたいとなった時に詰みます

マイグレーションにはどのスキーマバージョンからどのバージョンに移行するかの指定が必要で、上記の例だと作成したDBにスキーマバージョンが定義されないことになるためマイグレーション不可となるからです。

なのでモデルのバージョン管理も兼ねたVersionedSchemaクラスを定義しておき、これを.modelContainer()モディファイアに渡してあげる形にすると後になって頭を抱えずに済むと思います。

struct SchemaV1: VersionedSchema {

    static let versionIdentifier = Schema.Version(1, 0, 0) // ここ
    
    static var models: [any PersistentModel.Type] {
        [ItemDataV1.self]
    }
}

@main
struct App: App {

    var modelContainer: ModelContainer = {
        let cloudSchema = Schema(versionedSchema: SchemaV1.self)

        let cloudConfig = ModelConfiguration(
            "cloud",
            schema: cloudSchema,
            cloudKitDatabase: .automatic
        )

        do {
            return try ModelContainer(
                for: cloudSchema,
                configurations: [cloudConfig]
            )
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            RootView()
        }
        .modelContainer(modelContainer)
    }
}

iCloud同期とローカル限定のテーブルを併存させるTip

iCloudとの同期はModelConfigurationのcloudKitDatabaseを.automaticにすることで実現できます。

let cloudConfig = ModelConfiguration(
    schema: Schema,
    cloudKitDatabase: .automatic // .noneだと同期なし
)

私が開発したプロジェクトで、あるデータはiCloudで同期したいが別のデータはローカルだけで保持したいという場面に遭遇しました。
上記の通りModelConfigurationにはどちらかの設定しか適用できず、そこでiCloud同期用とローカル限定用のModelConfigurationおよびModelContainerを宣言することにしました。

.modelContainerモディファイアに関しても複数重ねることができないため、2つ目は.environmentモディファイアを使って無理やり子Viewでも参照できるようにしています。(最終的にシングルトンで管理することになりましたが...)

@main
struct MyApp: App {

    var modelContainer: ModelContainer = {
        // iCloud同期するモデル
        let cloudSchema = Schema(versionedSchema: CloudSchemaV1.self)
        let cloudConfig = ModelConfiguration(
            schema: cloudSchema,
            cloudKitDatabase: .automatic
        )
        ...
    }
    
    var localContainer: ModelContainer = {
        // iCloud同期しないモデル
        let localSchema = Schema(versionedSchema: LocalSchemaV1.self)
        let localConfig = ModelConfiguration(
            schema: localSchema,
            cloudKitDatabase: .none
        )
        ...
    }
    
    var body: some Scene {
        WindowGroup {
            RootView()
        }
        .modelContainer(modelContainer)
        .environment(\.localContainer, localContainer)
    }
}

ですがこれだとクラッシュします。

ModelConfigurationのイニシャライザには第一引数でnameを与えることができるのですが、エラーメッセージを見た限り同プロジェクトに複数のModelContainerがある場合はこの名前を与えないといけないっぽいです。

ModelConfiguration
/// A type that describes the configuration of an app's schema or specific group of models.
@available(swift 5.9)
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
public struct ModelConfiguration : Identifiable, Hashable {
...
    public init(
        _ name: String? = nil,
        schema: Schema? = nil,
        isStoredInMemoryOnly: Bool = false,
        allowsSave: Bool = true,
        groupContainer: ModelConfiguration.GroupContainer = .automatic,
        cloudKitDatabase: ModelConfiguration.CloudKitDatabase = .automatic
    )

ということで適当な名前を与えてあげれば無事iCloud同期DBとローカルDBの併存が実現できましたとさ。

@main
struct MyApp: App {

    var modelContainer: ModelContainer = {
        // iCloud同期するモデル
        let cloudSchema = Schema(versionedSchema: CloudSchemaV1.self)
        let cloudConfig = ModelConfiguration(
            "cloud",
            schema: cloudSchema,
            cloudKitDatabase: .automatic
        )
        ...
    }
    
    var localContainer: ModelContainer = {
        // iCloud同期しないモデル
        let localSchema = Schema(versionedSchema: LocalSchemaV1.self)
        let localConfig = ModelConfiguration(
            "local",
            schema: localSchema,
            cloudKitDatabase: .none
        )
        ...
    }
    
    var body: some Scene {
        WindowGroup {
            RootView()
        }
        .modelContainer(modelContainer)
        .environment(\.localContainer, localContainer)
    }
}

予約語にご注意

モデルクラスのプロパティ名にdescriptionは使えません。クラッシュの原因になります。静的解析仕事しろ

@Model
class TitleData {
	var id: UUID
	var title: String?
	var description: String? <- NG
}

おわりに

今後のSwiftDataにもご期待ください。

明日の and factory.inc Advent Calendar 2025@yst_i さんです :christmas_tree:

10
2
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
10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?