字节笔记本

2026年2月23日

SwiftData 与 CloudKit 集成实战指南

SwiftData 与 CloudKit 集成实战指南,介绍如何配置 iCloud 同步、修复模型兼容性问题,以及实现实时数据更新。本文基于 WWDC23 的实践经验,包含完整的代码示例。

准备工作

SwiftData 与 NSPersistentCloudKitContainer 类似(实际上底层就是基于它封装),因此配置 iCloud 同步非常简单,只需几个步骤即可开始。

首先确保已启用 iCloud entitlements,并勾选 CloudKit 选项。你需要有一个配置好的容器。

同时开启后台模式和远程通知,否则容器将无法正常加载。

完成这些配置后,CloudKit 同步会自动工作。SwiftData 会从 entitlements 中检测到配置并自动使用。

如需更精确的控制,可以在 ModelConfiguration 中显式指定容器标识符:

swift
@main
struct Brew_BookApp: App {
    let container: ModelContainer = {
        // 生产环境不要强制解包 👀
        try! ModelContainer(
            for: [Brewer.self, Brew.self],
            .init(
                cloudKitContainerIdentifier: "icloud.uk.co.alexanderlogan.samples.Brew-Book"
            )
        )
    }()

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

修复模型兼容性问题

启用 CloudKit 后,你的应用可能会突然崩溃。容器可能无法加载,控制台会充满错误信息。

典型的错误信息如下:

text
error: Store failed to load.
CloudKit integration requires that all attributes be optional, or have a default value set.
The following attributes are marked non-optional but do not have a default value:
Brew: brewDate
Brew: brewIdentifier
Brew: rating
Brew: type

CloudKit integration requires that all relationships be optional, the following are not:
Brewer: brews

CloudKit integration does not support unique constraints.
The following entities are constrained:
Brew: brewIdentifier

NSPersistentCloudKitContainer 一样,我们需要对数据模型进行一些修改才能与云端正常工作。

主要注意事项:

  • 唯一约束不受支持 - 需要移除 @Attribute(.unique)
  • 关系必须是可选的 - 即使默认值为空数组
  • 所有属性必须有默认值 - 显式设置或使用可选类型

模型改造示例

Brewer 模型改造:

swift
// 改造前
@Model
final class Brewer {
    var name: String
    @Relationship(.cascade, inverse: \Brew.brewer)
    var brews: [Brew]

    init(name: String) {
        self.name = name
    }
}
swift
// 改造后
@Model
final class Brewer {
    var name: String = ""
    @Relationship(.cascade, inverse: \Brew.brewer)
    var brews: [Brew]? = []

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

Brew 模型改造:

swift
// 改造前
@Model
final class Brew {
    @Attribute(.unique) var brewIdentifier: UUID
    var type: BrewType.RawValue
    var rating: Int
    var brewDate: Date
    var brewer: Brewer?

    init(
        brewIdentifier: UUID = .init(),
        type: BrewType,
        rating: Int,
        brewDate: Date
    ) {
        self.brewIdentifier = brewIdentifier
        self.type = type.rawValue
        self.rating = rating
        self.brewDate = brewDate
    }
}
swift
// 改造后
@Model
final class Brew {
    var brewIdentifier: UUID = .init()
    var type: BrewType.RawValue = BrewType.espresso.rawValue
    var rating: Int = 5
    var brewDate: Date = Date()
    var brewer: Brewer? = nil

    init(
        brewIdentifier: UUID = .init(),
        type: BrewType,
        rating: Int,
        brewDate: Date
    ) {
        self.brewIdentifier = brewIdentifier
        self.type = type.rawValue
        self.rating = rating
        self.brewDate = brewDate
    }
}

完成这些修改后,在已登录 iCloud 的设备上运行应用,保存一些数据,然后删除应用并重新安装,内容将会从云端恢复!

实现实时更新

你可能会注意到,即使云端有内容,应用首次打开时也不会显示任何内容。这是因为 @Query 在视图安装时不会自动响应来自云端的推送通知。

不过有一个巧妙的解决方法:利用底层的 NSPersistentCloudKitContainer.eventChangedNotification 来检测 CloudKit 更新事件,并强制刷新查询。

在 brewers 列表中添加以下功能:

swift
.onReceive(NotificationCenter.default.publisher(
    for: NSPersistentCloudKitContainer.eventChangedNotification
)) { notification in
    guard let event = notification.userInfo?[
        NSPersistentCloudKitContainer.eventNotificationUserInfoKey
    ] as? NSPersistentCloudKitContainer.Event else {
        return
    }

    if event.endDate != nil && event.type == .import {
        // TODO
    }
}

这里我们监听 eventChangedNotification,检查事件类型是否为 import(数据下载),并且事件已经结束。

现在来看关键技巧:

当这个通知触发时,数据已经下载完成,但 @Query 不会自动更新。我们可以通过手动执行一次 fetch 来"欺骗"查询更新:

swift
.onReceive(NotificationCenter.default.publisher(
    for: NSPersistentCloudKitContainer.eventChangedNotification
)) { notification in
    guard let event = notification.userInfo?[
        NSPersistentCloudKitContainer.eventNotificationUserInfoKey
    ] as? NSPersistentCloudKitContainer.Event else {
        return
    }

    if event.endDate != nil && event.type == .import {
        Task { @MainActor in
            let brewersFetchDescriptor = FetchDescriptor<Brewer>(
                predicate: nil,
                sortBy: [.init(\.name)]
            )
            _ = try? context.fetch(brewersFetchDescriptor)
        }
    }
}

当这个 fetch 执行时,它会同时触发 @Query 的更新。现在,当你全新安装应用时,内容会从云端获取并按预期更新,后续的更新也会正常工作。

总结

SwiftData 与 CloudKit 的集成大大简化了 iOS 应用的云端数据同步。主要要点:

  1. 配置简单 - 只需开启 entitlements 和后台模式
  2. 模型限制 - 移除唯一约束,设置默认值,使用可选关系
  3. 实时更新 - 利用 eventChangedNotification 实现数据自动刷新

完整的示例代码可以在 GitHub 上找到。


原文作者:Alex Logan (@SwiftyAlex) 原文链接:https://alexanderlogan.co.uk/blog/wwdc23/08-cloudkit-swift-data

分享: