mcpField
A
axiom-swiftdata-migration

axiom-swiftdata-migration

github.com/charleswiltgen数据与数据库1.2k187 次安装今天更新
代码仓库

关于 axiom-swiftdata-migration

面向 SwiftData 开发者的迁移指南,涵盖轻量迁移与自定义迁移、关系保留和升级验证。内容着重说明迁移阶段的访问限制及实用测试方法。

主要功能

  1. 01轻量迁移与自定义迁移判断
  2. 02属性类型变更的两阶段方案
  3. 03关系迁移模式
  4. 04VersionedSchema 设置
  5. 05迁移测试清单

适用场景

  1. 01更改持久化属性类型
  2. 02重组模型关系
  3. 03为重复数据添加唯一约束
  4. 04在真机上测试升级

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftdata-migration

安装后在 Claude Code 中输入/axiom-swiftdata-migration确认已加载。

详细介绍

SwiftData 自定义架构迁移

概述

SwiftData 架构迁移可在模型发生变化时安全地迁移数据。核心原则:SwiftData 的 willMigrate 只能访问旧模型,didMigrate 只能访问新模型——你永远无法同时访问两者。这一限制决定了所有迁移策略。

要求:iOS 17+、Swift 5.9+ 目标:iOS 26+(支持 propertiesToFetch 等功能)

何时需要自定义迁移

轻量级迁移(自动)

SwiftData 可自动迁移以下变更:

  • ✅ 添加新的可选属性
  • ✅ 添加带默认值的新必需属性
  • ✅ 移除属性
  • ✅ 重命名属性(使用 @Attribute(originalName:))
  • ✅ 更改关系删除规则
  • ✅ 添加新模型
自定义迁移(本技能)

以下情况需要自定义迁移:

  • ❌ 更改属性类型(String → AttributedString、Int → String)
  • ❌ 将可选属性改为必需属性(必须为现有的空值填充数据)
  • ❌ 复杂的关系重构
  • ❌ 数据转换(拆分/合并字段)
  • ❌ 添加唯一性约束时进行去重

示例提示词

以下是开发者实际会提出的问题,本技能旨在解答这些问题:

1. “我需要将属性从 String 改为 AttributedString。如何在迁移现有数据时保留关系?”

→ 本技能介绍了可绕过 willMigrate/didMigrate 限制的两阶段迁移模式

2. “我的模型具有级联删除的一对多关系。如何在类型变更迁移期间保留这种关系?”

→ 本技能讲解如何预取关系,以及如何在不同架构版本间维护反向关系

3. “Tags 和 Notes 之间有多对多关系。迁移失败并提示 ‘Expected only Arrays for Relationships’。这是怎么回事?”

→ 本技能介绍了显式反向关系的要求,以及 iOS 17.0 的字母顺序命名错误

4. “我需要重命名一个模型,但保留其所有关系。”

→ 本技能展示了用于轻量级迁移的 @Attribute(originalName:) 模式

5. “我的迁移在模拟器上运行正常,但在有现存数据的真机上崩溃。”

→ 本技能强调了真机测试,并解释了为什么模拟器测试成功并不能保证生产环境安全

6. “为什么我必须将所有模型都复制到每个 VersionedSchema 中,包括那些没有变化的模型?”

→ 本技能解释了 SwiftData 的设计:每个 VersionedSchema 都是完整快照,而非差异集

7. “我遇到了 ‘The model used to open the store is incompatible with the one used to create the store’ 错误。”

→ 本技能提供了排查架构版本不匹配问题的步骤

8. “发布到生产环境之前,如何测试 SwiftData 迁移?”

→ 本技能介绍了迁移测试工作流、真机测试要求和验证策略


willMigrate/didMigrate 的限制

重要:这是决定所有 SwiftData 迁移模式的架构约束。

可访问的内容
static let migrateV1toV2 = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        // ✅ CAN access: SchemaV1 models (old)
        let v1Notes = try context.fetch(FetchDescriptor<SchemaV1.Note>())

        // ❌ CANNOT access: SchemaV2 models
        // SchemaV2.Note doesn't exist yet
    },
    didMigrate: { context in
        // ✅ CAN access: SchemaV2 models (new)
        let v2Notes = try context.fetch(FetchDescriptor<SchemaV2.Note>())

        // ❌ CANNOT access: SchemaV1 models
        // SchemaV1.Note is gone
    }
)
这为何重要

你无法在单个迁移阶段中直接将数据从旧类型转换为新类型。例如:

// ❌ IMPOSSIBLE - you can't do this in one stage
willMigrate: { context in
    let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
    for oldNote in oldNotes {
        let newNote = SchemaV2.Note()  // ❌ Doesn't exist yet!
        newNote.content = oldNote.contentAsAttributedString()
    }
}

解决方案:使用两阶段迁移模式(下文将介绍)。


核心模式

模式 1:基本的 VersionedSchema 设置

每个不同的架构版本都必须定义为 VersionedSchema。

import SwiftData

enum NotesSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Note.self, Folder.self, Tag.self]  // ALL models, even if unchanged
    }

    @Model
    final class Note {
        @Attribute(.unique) var id: String
        var title: String
        var content: String  // Original type
        var createdAt: Date

        @Relationship(deleteRule: .nullify, inverse: \Folder.notes)
        var folder: Folder?

        @Relationship(deleteRule: .nullify, inverse: \Tag.notes)
        var tags: [Tag] = []

        init(id: String, title: String, content: String, createdAt: Date) {
            self.id = id
            self.title = title
            self.content = content
            self.createdAt = createdAt
        }
    }

    @Model
    final class Folder {
        @Attribute(.unique) var id: String
        var name: String

        @Relationship(deleteRule: .cascade)
        var notes: [Note] = []

        init(id: String, name: String) {
            self.id = id
            self.name = name
        }
    }

    @Model
    final class Tag {
        @Attribute(.unique) var id: String
        var name: String

        @Relationship(deleteRule: .nullify)
        var notes: [Note] = []

        init(id: String, name: String) {
            self.id = id
            self.name = name
        }
    }
}
关键模式
  • 完整快照:包含所有模型,即使有些模型没有变化
  • 语义化版本控制:使用 Schema.Version(major, minor, patch)
  • 显式初始化器:SwiftData 不会自动合成初始化器
  • 反向关系:在关系的两端都进行指定,以实现双向关系

模式 2:类型变更的两阶段迁移

适用场景:更改属性类型(String → AttributedString、Int → String 等)

问题

我们要将 Note.content 从 String 改为 AttributedString,但无法同时访问新旧两种类型。

解决方案

使用同时包含两个属性的中间架构版本(V1.1)。

// Stage 1: V1 → V1.1 (Add new property alongside old)
enum NotesSchemaV1_1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 1, 0)

    static var models: [any PersistentModel.Type] {
        [Note.self, Folder.self, Tag.self]
    }

    @Model
    final class Note {
        @Attribute(.unique) var id: String
        var title: String

        // OLD property (to be deprecated)
        @Attribute(originalName: "content")
        var contentOld: String = ""

        // NEW property (target type)
        var contentNew: AttributedString?

        var createdAt: Date

        @Relationship(deleteRule: .nullify, inverse: \Folder.notes)
        var folder: Folder?

        @Relationship(deleteRule: .nullify, inverse: \Tag.notes)
        var tags: [Tag] = []

        init(id: String, title: String, contentOld: String, createdAt: Date) {
            self.id = id
            self.title = title
            self.contentOld = contentOld
            self.createdAt = createdAt
        }
    }

    // Folder and Tag unchanged (copy from V1)
    @Model final class Folder { /* same as V1 */ }
    @Model final class Tag { /* same as V1 */ }
}

// Stage 2: V1.1 → V2 (Transform data, remove old property)
enum NotesSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Note.self, Folder.self, Tag.self]
    }

    @Model
    final class Note {
        @Attribute(.unique) var id: String
        var title: String

        // Renamed from contentNew
        @Attribute(originalName: "contentNew")
        var content: AttributedString?

        var createdAt: Date

        @Relationship(deleteRule: .nullify, inverse: \Folder.notes)
        var folder: Folder?

        @Relationship(deleteRule: .nullify, inverse: \Tag.notes)
        var tags: [Tag] = []

        init(id: String, title: String, content: AttributedString?, createdAt: Date) {
            self.id = id
            self.title = title
            self.content = content
            self.createdAt = createdAt
        }
    }

    @Model final class Folder { /* same as V1 */ }
    @Model final class Tag { /* same as V1 */ }
}
迁移计划
enum NotesMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [NotesSchemaV1.self, NotesSchemaV1_1.self, NotesSchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV1_1, migrateV1_1toV2]
    }

    // Stage 1: Lightweight migration (adds contentNew)
    static let migrateV1toV1_1 = MigrationStage.lightweight(
        fromVersion: NotesSchemaV1.self,
        toVersion: NotesSchemaV1_1.self
    )

    // Stage 2: Custom migration (transform String → AttributedString)
    static let migrateV1_1toV2 = MigrationStage.custom(
        fromVersion: NotesSchemaV1_1.self,
        toVersion: NotesSchemaV2.self,
        willMigrate: { context in
            // Transform data while we still have access to V1.1 models
            var fetchDesc = FetchDescriptor<NotesSchemaV1_1.Note>()

            // Prefetch relationships to preserve them
            fetchDesc.relationshipKeyPathsForPrefetching = [\.folder, \.tags]

            let notes = try context.fetch(fetchDesc)

            for note in notes {
                // Convert String → AttributedString
                note.contentNew = try? AttributedString(markdown: note.contentOld)
            }

            try context.save()
        },
        didMigrate: nil
    )
}
应用迁移计划
@main
struct NotesApp: App {
    let container: ModelContainer = {
        do {
            let schema = Schema(versionedSchema: NotesSchemaV2.self)
            return try ModelContainer(
                for: schema,
                migrationPlan: NotesMigrationPlan.self
            )
        } catch {
            fatalError("Failed to create container: \(error)")
        }
    }()

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

模式 3:迁移多对多关系

适用场景 存在多对多关系(标签 ↔ 笔记)

关键要求
  1. 显式反向关系 SwiftData 不会推断多对多关系
  2. 两侧都使用数组 不能为可选类型,必须是数组
  3. iOS 17.0 错误的解决方法 字母顺序命名问题
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Note.self, Tag.self]
    }

    @Model
    final class Note {
        @Attribute(.unique) var id: String
        var title: String

        // Many-to-many: MUST specify inverse
        @Relationship(deleteRule: .nullify, inverse: \Tag.notes)
        var tags: [Tag] = []  // ✅ Array with default value

        init(id: String, title: String) {
            self.id = id
            self.title = title
        }
    }

    @Model
    final class Tag {
        @Attribute(.unique) var id: String
        var name: String

        // Many-to-many: MUST specify inverse
        @Relationship(deleteRule: .nullify, inverse: \Note.tags)
        var notes: [Note] = []  // ✅ Array with default value

        init(id: String, name: String) {
            self.id = id
            self.name = name
        }
    }
}
iOS 17.0 字母顺序错误的解决方法

在 iOS 17.0 中,如果模型名称按字母顺序排列,多对多关系可能会失败(例如,Actor ↔ Movie 可行,但 Movie ↔ Person 不可行)。

解决方法 为关系数组提供默认值:

@Relationship(deleteRule: .nullify, inverse: \Movie.actors)
var actors: [Actor] = []  // ✅ Default value prevents bug

已在 iOS 17.1+ 中修复

添加联结表元数据

如果需要为关系添加其他字段(例如,“添加此标签的时间是什么时候?”),请使用显式联结模型:

@Model
final class NoteTag {
    @Attribute(.unique) var id: String
    var addedAt: Date  // Metadata on relationship

    @Relationship(deleteRule: .cascade)
    var note: Note?

    @Relationship(deleteRule: .cascade)
    var tag: Tag?

    init(id: String, note: Note, tag: Tag, addedAt: Date) {
        self.id = id
        self.note = note
        self.tag = tag
        self.addedAt = addedAt
    }
}

@Model
final class Note {
    @Attribute(.unique) var id: String
    var title: String

    @Relationship(deleteRule: .cascade)
    var noteTags: [NoteTag] = []  // One-to-many to junction

    var tags: [Tag] {
        noteTags.compactMap { $0.tag }
    }
}

@Model
final class Tag {
    @Attribute(.unique) var id: String
    var name: String

    @Relationship(deleteRule: .cascade)
    var noteTags: [NoteTag] = []  // One-to-many to junction

    var notes: [Note] {
        noteTags.compactMap { $0.note }
    }
}

模式 4:迁移期间预取关系

适用场景 迁移带有关系的模型,以避免 N+1 查询

static let migrateV1toV2 = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        var fetchDesc = FetchDescriptor<SchemaV1.Note>()

        // Prefetch relationships (iOS 26+)
        fetchDesc.relationshipKeyPathsForPrefetching = [\.folder, \.tags]

        // Only fetch properties you need (iOS 26+)
        fetchDesc.propertiesToFetch = [\.title, \.content]

        let notes = try context.fetch(fetchDesc)

        // Relationships are already loaded - no N+1
        for note in notes {
            let folderName = note.folder?.name  // ✅ Already in memory
            let tagCount = note.tags.count  // ✅ Already in memory
        }

        try context.save()
    },
    didMigrate: nil
)
性能影响
Without prefetching:
- 1 query to fetch notes
- N queries to fetch each note's folder
- N queries to fetch each note's tags
= 1 + N + N queries

With prefetching:
- 1 query to fetch notes
- 1 query to fetch all folders
- 1 query to fetch all tags
= 3 queries total

模式 5:重命名属性

适用场景 想要重命名属性且不丢失数据

enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Note.self]
    }

    @Model
    final class Note {
        @Attribute(.unique) var id: String
        var title: String  // Original name
    }
}

enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Note.self]
    }

    @Model
    final class Note {
        @Attribute(.unique) var id: String

        // Renamed from "title" to "heading"
        @Attribute(originalName: "title")
        var heading: String
    }
}

// Migration plan (lightweight migration)
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )
}

原理 SwiftData 会识别 originalName,并在轻量级迁移期间保留数据。


模式 6:对唯一约束进行去重

适用场景 为包含重复值的字段添加 @Attribute(.unique)

enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Trip.self]
    }

    @Model
    final class Trip {
        @Attribute(.unique) var id: String
        var name: String  // ❌ Not unique, has duplicates

        init(id: String, name: String) {
            self.id = id
            self.name = name
        }
    }
}

enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Trip.self]
    }

    @Model
    final class Trip {
        @Attribute(.unique) var id: String
        @Attribute(.unique) var name: String  // ✅ Now unique

        init(id: String, name: String) {
            self.id = id
            self.name = name
        }
    }
}

enum TripMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self,
        willMigrate: { context in
            // Deduplicate before adding unique constraint
            let trips = try context.fetch(FetchDescriptor<SchemaV1.Trip>())

            var seenNames = Set<String>()
            for trip in trips {
                if seenNames.contains(trip.name) {
                    // Duplicate - delete or rename
                    context.delete(trip)
                } else {
                    seenNames.insert(trip.name)
                }
            }

            try context.save()
        },
        didMigrate: nil
    )
}

测试迁移

必须执行的测试清单
  • 测试全新安装(从 V1 运行所有迁移,直至最新版本)
  • 测试从每个先前版本升级
  • 在真实设备上测试(不能只用模拟器)
  • 验证迁移后的关系完整性
  • 检查是否丢失数据(比较迁移前后的记录数)
  • 使用接近生产规模的数据集进行测试
为什么仅在模拟器中测试还不够

模拟器行为 重建时会删除数据库,因此始终使用全新架构

真实设备行为 更新期间会保留持久化数据库,因此架构必须匹配

// ❌ WRONG - only testing in simulator
// You rebuild → simulator deletes database → fresh install
// Migration code never runs!

// ✅ CORRECT - test on real device
// 1. Install v1 build on device
// 2. Create sample data
// 3. Install v2 build (with migration)
// 4. Verify data preserved
测试工作流

将任何迁移部署到生产环境之前:

1. 创建测试数据集

准备代表迁移前状态的测试数据:

  • 最小数据集 - 10-20 条记录,涵盖所有关系类型
  • 逼真数据集 - 1,000+ 条记录,符合生产环境规模
  • 边界情况 - 空关系、关系数量达到上限、可选字段
2. 在模拟器中测试

使用测试数据运行迁移:

// Create test data in V1 schema
let v1Container = try ModelContainer(for: Schema(versionedSchema: SchemaV1.self))
// ... populate test data ...

// Run migration
let v2Container = try ModelContainer(
    for: Schema(versionedSchema: SchemaV2.self),
    migrationPlan: MigrationPlan.self
)

验证:

  • 所有关系均得到保留
  • 没有数据丢失(比较迁移前后的记录数)
  • 新字段已正确填充
  • 使用逼真规模的数据集时,性能可以接受
3. 在真实设备上测试

重要 - 在模拟器中成功并不代表在生产环境中安全。

# Workflow:
1. Install v1 build on real device
2. Create 100+ records with relationships
3. Verify data exists
4. Install v2 build (over existing app, don't delete)
5. Launch app
6. Verify:
   - App launches without crash
   - All 100+ records still exist
   - Relationships intact
   - New fields populated
4. 使用生产数据验证(如可行)

如果可以访问生产数据:

  • 将生产数据库复制到开发环境
  • 对副本运行迁移
  • 验证没有数据损坏
  • 检查使用生产规模数据集时的性能

如果迁移失败,请参阅 axiom-swiftdata-migration-diag 获取调试工具。

迁移测试模式
import Testing
import SwiftData

@Test func testMigrationFromV1ToV2() throws {
    // 1. Create V1 data
    let v1Schema = Schema(versionedSchema: SchemaV1.self)
    let v1Config = ModelConfiguration(isStoredInMemoryOnly: true)
    let v1Container = try ModelContainer(for: v1Schema, configurations: v1Config)

    let context = v1Container.mainContext
    let note = SchemaV1.Note(id: "1", title: "Test", content: "Original")
    context.insert(note)
    try context.save()

    // 2. Run migration to V2
    let v2Schema = Schema(versionedSchema: SchemaV2.self)
    let v2Container = try ModelContainer(
        for: v2Schema,
        migrationPlan: MigrationPlan.self,
        configurations: v1Config
    )

    // 3. Verify data migrated
    let v2Context = v2Container.mainContext
    let notes = try v2Context.fetch(FetchDescriptor<SchemaV2.Note>())

    #expect(notes.count == 1)
    #expect(notes.first?.content != nil)  // String → AttributedString
}

决策树:轻量级迁移还是自定义迁移

What change are you making?
├─ Adding optional property → Lightweight ✓
├─ Adding required property with default → Lightweight ✓
├─ Renaming property (with originalName) → Lightweight ✓
├─ Removing property → Lightweight ✓
├─ Changing relationship delete rule → Lightweight ✓
├─ Adding new model → Lightweight ✓
├─ Changing property type → Custom (two-stage) ✗
├─ Making optional → required → Custom (populate nulls first) ✗
├─ Adding unique constraint (duplicates exist) → Custom (deduplicate first) ✗
└─ Complex relationship restructure → Custom ✗

常见错误

❌ 忘记在 VersionedSchema 中包含所有模型
enum SchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Note.self]  // ❌ WRONG: Missing Folder and Tag
    }
}

// ✅ CORRECT: Include ALL models
enum SchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Note.self, Folder.self, Tag.self]  // ✅ Even if unchanged
    }
}

原因 每个 VersionedSchema 都是数据模型的完整快照,而不是差异。


❌ 尝试在 didMigrate 中访问旧模型
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: nil,
    didMigrate: { context in
        // ❌ CRASH: SchemaV1.Note doesn't exist here
        let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
    }
)

// ✅ CORRECT: Use willMigrate for old models
static let migrate = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: { context in
        // ✅ SchemaV1.Note exists here
        let oldNotes = try context.fetch(FetchDescriptor<SchemaV1.Note>())
    },
    didMigrate: nil
)

❌ 未在真实设备上使用真实数据进行测试
// ❌ WRONG: Simulator success ≠ production safety
// Rebuild simulator → database deleted → fresh install
// Migration never actually runs!

// ✅ CORRECT: Test migration path
// 1. Install v1 on real device
// 2. Create data (100+ records)
// 3. Install v2 with migration
// 4. Verify data preserved

❌ 多对多关系没有显式反向关系
// ❌ WRONG: SwiftData can't infer many-to-many
@Model
final class Note {
    var tags: [Tag] = []  // ❌ Missing inverse
}

// ✅ CORRECT: Explicit inverse
@Model
final class Note {
    @Relationship(deleteRule: .nullify, inverse: \Tag.notes)
    var tags: [Tag] = []  // ✅ Inverse specified
}

❌ 认为模拟器测试成功就等于生产环境成功

模拟器会在重建时删除数据库。真实设备会在更新期间保留持久化数据库。

影响 在模拟器中被隐藏的迁移错误,会导致 100% 的生产用户遇到崩溃。

修复方法 发布前务必在真实设备上测试。


调试失败的迁移

启用 Core Data SQL 调试
# In Xcode scheme, add argument:
-com.apple.coredata.swiftdata.debug 1

输出 在迁移期间显示实际的 SQL 查询

CoreData: sql: SELECT Z_PK, Z_ENT, Z_OPT, ZID, ZTITLE FROM ZNOTE
CoreData: sql: ALTER TABLE ZNOTE ADD COLUMN ZCONTENT TEXT
常见错误消息
错误可能原因修复方法
"Expected only Arrays for Relationships"缺少多对多关系的反向关系添加 @Relationship(inverse:)
"The model used to open the store is incompatible"架构版本不匹配检查迁移计划中的 schemas 数组
"Failed to fulfill faulting for..."关系完整性遭到破坏在迁移期间预取关系
架构更改后应用在启动时崩溃VersionedSchema 中缺少模型包含所有模型

快速参考

基本迁移设置
// 1. Define versioned schemas
enum SchemaV1: VersionedSchema { /* models */ }
enum SchemaV2: VersionedSchema { /* models */ }

// 2. Create migration plan
enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )
}

// 3. Apply to container
let schema = Schema(versionedSchema: SchemaV2.self)
let container = try ModelContainer(
    for: schema,
    migrationPlan: MigrationPlan.self
)

资源

WWDC:2025-291、2023-10195

文档:/swiftdata

技能:axiom-swiftdata、axiom-swiftdata-migration-diag、axiom-database-migration


创建时间 2025-12-09 目标平台 iOS 17+(重点介绍 iOS 26+ 功能) 框架 SwiftData(Apple) Swift 5.9+

评论

支持 Markdown