WWDC Quick Look 💓 By SwiftGGTeam
What's new in Core Data

What's new in Core Data

观看原视频

Highlight

Core Data 在 iOS 17 引入了 Composite Attributes(组合属性)来组织结构化数据、Staged Migration(分阶段迁移)来处理复杂模型变更、以及 Deferred Migration(延迟迁移)来避免启动时的长时间等待。

核心内容

数据模型随着 app 迭代会变得越来越复杂。以前遇到这些问题时,开发者往往要在”写一堆迁移代码”和”放弃 Core Data 换用其他方案”之间做选择。

Composite Attributes 解决的是结构化数据的存储问题。以前要把一组相关属性拍平成多个独立字段,或者用 Transformable 类型自己写序列化代码。Composite Attributes 让你可以把一组相关属性封装成一个组合属性,像字典一样访问,还能直接用 keypath 做查询条件。

Staged Migration 解决的是复杂模型变更的迁移问题。有些变更(比如把一个实体的属性拆成独立实体)超出了轻量级迁移的能力范围。以前只能写自定义迁移映射,代码量大且容易出错。Staged Migration 让你把复杂变更拆成多个轻量级迁移可以处理的步骤,Core Data 自动按顺序执行。

Deferred Migration 解决的是迁移阻塞 UI 的问题。有些轻量级迁移虽然能自动完成,但清理旧数据需要时间。以前这会在 app 启动时卡住。Deferred Migration 让清理工作延迟到后台执行,app 先用上新 schema,数据清理稍后再做。

详细内容

Composite Attributes

00:56)Composite Attributes 是 Core Data 的新属性类型,用于封装结构化数据。

假设有一个 Aircraft 实体,以前用 Transformable 类型存储颜色方案:

@NSManaged public var colors: Data?  // Transformable,需要自定义 transformer

现在可以用 Composite Attribute:

@NSManaged public var colorScheme: [String: Any]?

在 Xcode 模型编辑器中定义 composite attribute:

  1. 添加 composite attribute,命名为 colorScheme
  2. 在其中添加三个 String 子属性:primarysecondarytertiary
  3. Aircraft 实体中添加 colorScheme 属性,类型设为 colorScheme

设置 composite attribute:

private func addAircraft() {
    viewContext.performAndWait {
        let newAircraft = Aircraft(context: viewContext)
        newAircraft.tailNumber = tailNumber
        newAircraft.aircraftType = aircraftType
        newAircraft.aircraftClass = aircraftClass
        newAircraft.aircraftCategory = aircraftCategory

        newAircraft.colorScheme = [
            "primary": primaryColor.rawValue,
            "secondary": secondaryColor.rawValue,
            "tertiary": tertiaryColor.rawValue
        ]

        do {
            try viewContext.save()
        } catch {
            // 处理错误
        }
    }
}

关键点:

  • 用字典语法访问 composite attribute 的子属性
  • 键名与模型编辑器中定义的属性名一致

用 keypath 查询 composite attribute:

private func findAircraft(with color: String) {
    viewContext.performAndWait {
        let fetchRequest = Aircraft.fetchRequest()
        fetchRequest.predicate = NSPredicate(
            format: "colorScheme.primary == %@", color
        )

        do {
            let fetchedResults = try viewContext.fetch(fetchRequest)
            // ...
        } catch {
            // 处理错误
        }
    }
}

关键点:

  • 使用 colorScheme.primary 这样的 namespaced keypath
  • 可以直接在 NSPredicate 中查询子属性
  • 这是 Transformable 属性做不到的

Composite Attributes 还可以嵌套,一个 composite attribute 可以包含其他 composite attribute。元素只能是 NSAttributeDescription 或嵌套的 NSCompositeAttributeDescription,不能包含 relationship。

性能方面,如果 fetching 一个实体后几乎总是需要访问它的某个 relationship,可以把那个 relationship 的数据改成 composite attribute。这样可以避免 faulting 带来的额外查询。

Staged Migration

07:36)当模型变更超出轻量级迁移能力时,使用 Staged Migration。

核心思路:把复杂的非轻量级变更,拆成多个轻量级迁移可以处理的步骤。

示例场景:Aircraft 实体有一个 flightData 二进制属性,需要把它拆成独立的 FlightData 实体。

分解步骤:

  1. ModelV1(原始):AircraftflightData 属性
  2. ModelV2:添加 FlightData 实体,与 Aircraft 建立 relationship,轻量级迁移可以完成
  3. ModelV3:删除 AircraftflightData 属性,数据已通过自定义代码迁移到 FlightData

创建模型引用:

let v1ModelChecksum = "kk8XL4OkE7gYLFHTrH6W+EhTw8w14uq1klkVRPiuiAk="
let v1ModelReference = NSManagedObjectModelReference(
    modelName: "modelV1",
    in: Bundle.main,
    versionChecksum: v1ModelChecksum
)

let v2ModelChecksum = "PA0Gbxs46liWKg7/aZMCBtu9vVIF6MlskbhhjrCd7ms="
let v2ModelReference = NSManagedObjectModelReference(
    modelName: "modelV2",
    in: Bundle.main,
    versionChecksum: v2ModelChecksum
)

let v3ModelChecksum = "iWKg7bxs46g7liWkk8XL4OkE7gYL/FHTrH6WF23Jhhs="
let v3ModelReference = NSManagedObjectModelReference(
    modelName: "modelV3",
    in: Bundle.main,
    versionChecksum: v3ModelChecksum
)

关键点:

  • versionChecksumNSManagedObjectModel.versionChecksum 获取
  • 也可以在 Xcode build log 中搜索 “version checksum”
  • versioned model 的 checksum 也在 VersionInfo.plist

创建迁移阶段:

// V1 → V2 是轻量级变更(添加属性/实体)
let lightweightStage = NSLightweightMigrationStage([v1ModelChecksum])
lightweightStage.label = "V1 to V2: Add flightData attribute"

// V2 → V3 需要自定义代码迁移数据
let customStage = NSCustomMigrationStage(
    migratingFrom: v2ModelReference,
    to: v3ModelReference
)
customStage.label = "V2 to V3: Denormalize model with FlightData entity"

在自定义阶段中迁移数据:

customStage.willMigrateHandler = { migrationManager, currentStage in
    guard let container = migrationManager.container else {
        return
    }

    let context = container.newBackgroundContext()
    try context.performAndWait {
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Aircraft")
        fetchRequest.predicate = NSPredicate(format: "flightData != nil")

        do {
            let fetchedResults = try context.fetch(fetchRequest)
            for airplane in fetchedResults {
                guard let airplane = airplane as? NSManagedObject else { continue }

                let fdEntity = NSEntityDescription.insertNewObject(
                    forEntityName: "FlightData",
                    into: context
                )

                let flightData = airplane.value(forKey: "flightData")
                fdEntity.setValue(flightData, forKey: "data")
                fdEntity.setValue(airplane, forKey: "aircraft")
                airplane.setValue(nil, forKey: "flightData")
            }
            try context.save()
        } catch {
            // 处理错误
        }
    }
}

关键点:

  • 使用 NSFetchRequestResultNSManagedObject 泛型,因为迁移时子类可能不存在
  • willMigrateHandler 在轻量级迁移完成后、schema 更新前执行
  • 在 handler 中创建新实体、复制数据、建立 relationship
  • didMigrateHandler 在 schema 更新后执行(如果需要)

配置 staged migration:

let migrationStages = [lightweightStage, customStage]
let migrationManager = NSStagedMigrationManager(migrationStages)

let persistentContainer = NSPersistentContainer(
    path: "/path/to/store.sqlite",
    managedObjectModel: myModel
)

var storeDescription = persistentContainer.persistentStoreDescriptions.first
storeDescription?.setOption(
    migrationManager,
    forKey: NSPersistentStoreStagedMigrationManagerOptionKey
)

persistentContainer.loadPersistentStores { storeDescription, error in
    if let error = error {
        // 处理错误
    }
}

关键点:

  • NSStagedMigrationManager 管理整个迁移流程
  • 通过 NSPersistentStoreStagedMigrationManagerOptionKey 添加到 store options
  • Core Data 自动按顺序执行每个阶段

Deferred Migration

18:56)某些轻量级迁移的清理工作可以延迟执行。

启用延迟迁移:

let options = [
    NSPersistentStoreDeferredLightweightMigrationOptionKey: true,
    NSMigratePersistentStoresAutomaticallyOption: true,
    NSInferMappingModelAutomaticallyOption: true
]

let store = try coordinator.addPersistentStore(
    ofType: NSSQLiteStoreType,
    configurationName: nil,
    at: storeURL,
    options: options
)

在合适的时机完成延迟迁移:

// 通过 BGProcessingTask 在后台执行
let metadata = coordinator.metadata(for: store)
if metadata[NSPersistentStoreDeferredLightweightMigrationOptionKey] == true {
    coordinator.finishDeferredLightweightMigration()
}

关键点:

  • 轻量级迁移本身仍是同步的,只有清理工作被延迟
  • app 可以立即使用新 schema
  • 延迟迁移支持回退到 macOS Big Sur 和 iOS 14
  • 仅支持 SQLite store type
  • 适合的场景:删除属性/relationship、改变 entity hierarchy、改变 ordered relationship
  • 建议用 BGProcessingTask 在设备空闲时执行

Staged + Deferred 组合使用

22:08)复杂迁移可以组合两种技术。比如示例中的 ModelV3 删除 flightData 属性,这个清理工作可以延迟到后台执行。

核心启发

1. 地址/联系人结构化存储

  • 做什么:用 Composite Attributes 把地址的省、市、区、街道封装成一个 address 属性
  • 为什么值得做:比拍平成四个独立属性更直观,查询时可以用 address.city == %@ 直接过滤
  • 怎么开始:在 Xcode 模型编辑器中创建 composite attribute,包含 provincecitydistrictstreet 四个 String 子属性

2. 安全迁移大型用户数据库

  • 做什么:用 Staged Migration 把复杂的数据库变更拆成多步,避免一次性迁移导致启动崩溃
  • 为什么值得做:用户数据量大时,复杂迁移可能超时被系统杀死,分阶段执行更稳定
  • 怎么开始:分析模型变更,把非轻量级变更拆成”添加新结构→迁移数据→删除旧结构”三步,每步用一个 model version

3. 零阻塞启动体验

  • 做什么:用 Deferred Migration 把清理工作放到后台,app 启动后立即可用
  • 为什么值得做:用户打开 app 时如果卡在启动屏超过几秒就会流失,延迟迁移让启动保持快速
  • 怎么开始:在 store options 中加入 NSPersistentStoreDeferredLightweightMigrationOptionKey: true,注册 BGProcessingTask 在后台调用 finishDeferredLightweightMigration()

4. 从 Transformable 迁移到 Composite Attributes

  • 做什么:把现有的 Transformable 属性(如存储 JSON 编码的自定义类型)逐步替换为 Composite Attributes
  • 为什么值得做:Transformable 无法直接查询子字段,Composite Attributes 支持 NSPredicate keypath 查询,性能更好
  • 怎么开始:创建新的 composite attribute,在 Staged Migration 的 willMigrateHandler 中把 Transformable 数据解析并写入 composite 子属性

关联 Session

评论

GitHub Issues · utterances