https://alexanderlogan.co.uk/blog/wwdc23/08-cloudkit-swift-data
SwiftData具有与NSPersistentCloudKitContainer
类似的超能力,实际上我很确定它只是对其的封装,所以让我们来了解一下它们。
将您的数据放到云端
CloudKit已经很容易使用了一段时间,现在变得更加容易。只需几个步骤就可以开始同步到云端。
确保您已启用iCloud权限,并仔细检查CloudKit
是否已勾选。您还需要设置一个容器。
在这里顺便打开后台模式和远程通知,否则您的容器将无法加载。
一旦启用所有这些,CloudKit同步就会正常工作。它会从您的权限中检测到它,并使用它。
为了获得更明确的控制,您可以显式地为您的ModelConfiguration
提供容器标识符。
我们之前见过这段代码,但为了以防万一,这里再次展示。
@main
struct Brew_BookApp: App {
let container: ModelContainer = {
// Don't force unwrap for real 👀
try! ModelContainer(
for: [Brewer.self, Brew.self],
.init(
cloudKitContainerIdentifier: "icloud.uk.co.alexanderlogan.samples.Brew-Book"
)
)
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
@main
struct Brew_BookApp: App {
let container: ModelContainer = {
// Don't force unwrap for real 👀
try! ModelContainer(
for: [Brewer.self, Brew.self],
.init(
cloudKitContainerIdentifier: "icloud.uk.co.alexanderlogan.samples.Brew-Book"
)
)
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
修复我们的模型
如果您启用了这个功能,您的应用很可能会突然崩溃。您的容器可能无法加载,控制台会充满错误。
如果您还没有亲自运行过,我在这里添加了一个错误示例 - 它们非常有用!
error: Store failed to load. <NSPersistentStoreDescription: 0x600000c5d6b0> (type: SQLite, url: file:///Users/alex/Library/Developer/CoreSimulator/Devices/95D42E8D-CA19-481B-9150-ACD1642F8194/data/Containers/Data/Application/6564C0E2-2907-490F-A5E0-E8ED505C705B/Library/Application%20Support/default.store) with error = Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=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} with userInfo {
NSLocalizedFailureReason = "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:\nBrew: brewDate\nBrew: brewIdentifier\nBrew: rating\nBrew: type\nCloudKit integration requires that all relationships be optional, the following are not:\nBrewer: brews\nCloudKit integration does not support unique constraints. The following entities are constrained:\nBrew: brewIdentifier";
}
error: Store failed to load. <NSPersistentStoreDescription: 0x600000c5d6b0> (type: SQLite, url: file:///Users/alex/Library/Developer/CoreSimulator/Devices/95D42E8D-CA19-481B-9150-ACD1642F8194/data/Containers/Data/Application/6564C0E2-2907-490F-A5E0-E8ED505C705B/Library/Application%20Support/default.store) with error = Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=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} with userInfo {
NSLocalizedFailureReason = "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:\nBrew: brewDate\nBrew: brewIdentifier\nBrew: rating\nBrew: type\nCloudKit integration requires that all relationships be optional, the following are not:\nBrewer: brews\nCloudKit integration does not support unique constraints. The following entities are constrained:\nBrew: brewIdentifier";
}
就像使用NSPersistentCloudkitContainer
一样,我们必须对数据模型进行一些更改,以便在云端正常工作。
需要注意的主要事项是不支持唯一约束,关系必须是可选的(即使您将它们默认为空数组),并且所有值都必须有默认值。
让我们通过一个已存在的模型示例,将其修改为CloudKit友好的模型。
我们的第一个模型Brewer,只需要设置默认值并将关系设置为可选。
// After
@Model final class Brewer {
var name: String = ""
@Relationship(.cascade, inverse: \Brew.brewer)
var brews: [Brew]? = []
init(name: String) {
self.name = name
}
}
// After
@Model final class Brewer {
var name: String = ""
@Relationship(.cascade, inverse: \Brew.brewer)
var brews: [Brew]? = []
init(name: String) {
self.name = name
}
}
接下来,让我们修复Brew模型。
这个稍微复杂一些,因为我们有一个唯一参数,它必须被移除。
// Before
@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
}
}
// After
@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
}
}
// Before
@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
}
}
// After
@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
}
}
如果您现在在已登录的设备上运行应用,保存一些内容,删除应用,重新安装,您的内容将会回来!
实时更新
您会注意到当您最初打开应用时,即使云端有内容,也不会显示任何内容。这是因为@Query
在安装到视图后不会在从Web获得推送时自动更新 - 但有一个巧妙的技巧可以解决这个问题。
我们实际上可以使用底层的NSPersistentCloudKitContainer.eventChangedNotification
来检测由于CloudKit而更新的内容,并强制我们的查询更新。
让我们将此功能添加到我们的酿酒师列表中。
.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
}
}
.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(数据下载),并且我们有一个结束日期表示它已完成。
现在,来看技巧。
当此通知触发时,我们的数据已下载并准备就绪,但我们的查询不会更新。
我们可以通过触发手动获取来欺骗它更新。
.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)
}
}
}
.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)
}
}
}
当触发此获取时,它会欺骗查询也进行更新。
现在,当您从全新安装运行应用时,您的内容将从Web获取并按预期更新,后续更新也是如此。
数据存储的这个版本和仅本地版本的示例都可以在Github上找到。
如果您想阅读更多内容或分享您的经验,我是Twitter上的@SwiftyAlex。
参考链接
原文链接:https://alexanderlogan.co.uk/blog/wwdc23/08-cloudkit-swift-data