当前位置: 首页 > news >正文

SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决

在这里插入图片描述

0. 问题现象

我们 watchOS 中的 App 和 Widgets 共享同一个 SwiftData 底层数据库,但是在 App 中对数据库所做的更改并不能被 Widgets 所感知。换句话说,App 更新了数据但在 Widgets 中却看不到。

在这里插入图片描述

如上图所示:我们的 App 在切换至后台之前会随机更新当前的驻场英雄,而驻场英雄会在 Widget 中显示。不过,目前我们的 Widget 中却并未识别到任何驻场英雄,这是怎么回事?又该如何解决呢?

在本篇博文中,您将学到如下内容:

  • 0. 问题现象
  • 1. 示例代码
  • 2. 推本溯源
  • 3. 解决之道
  • 总结

本文编译及运行环境:Xcode 16 + watchOS 11。


1. 示例代码

首先是 SwiftData 数据模型:

import Foundation
import SwiftData@Model
class Hero {var hid: UUIDvar name: Stringvar power: Intvar residentCount: Int = 0var timestamp: Dateinit(name: String, power: Int) {self.hid = UUID()self.name = nameself.power = powertimestamp = .now}func update() {timestamp = .now}private static let HeroInfos: [(name: String, power: Int)] = [("黑悟空", 10000),("钢铁侠", 5000),("灭霸他爸", 500000),]@MainActorstatic func spawnHeros(forPreview: Bool = true) {let container = forPreview ? ModelContainer.preview : .sharedlet context = container.mainContextif !forPreview {let desc = FetchDescriptor<Hero>()if try! context.fetchCount(desc) > 0 {return}}for hero in HeroInfos {let new = Hero(name: hero.name, power: hero.power)context.insert(new)}try! context.save()}
}@Model
class Model {private static let UniqID = UUID(uuidString: "3788ABA9-043C-4D34-B119-5D69D486CBBA")!var mid: UUID@Relationship(deleteRule: .nullify)var residentHero: Hero?init(mid: UUID) {self.mid = midself.residentHero = nil}@MainActorstatic var shared: Model = {let context = ModelContainer.auto.mainContextlet predicate = #Predicate<Model> { model inmodel.mid == UniqID}let desc = FetchDescriptor(predicate: predicate)if let result = try! context.fetch(desc).first {return result} else {let new = Model(mid: UniqID)context.insert(new)try! context.save()return new}}()// 随机产生驻场英雄@MainActorfunc chooseResidentHero() {let context = ModelContainer.auto.mainContextlet desc = FetchDescriptor<Hero>(sortBy: [.init(\Hero.power)])if let hero = try! context.fetch(desc).randomElement() {residentHero = herohero.residentCount += 1try! context.save()}}
}

可以看到,我们的 App 由 Hero 和 Model 两种数据模型构成。其中,在 Model 里我们以关系(@Relationship)的形式将驻场英雄字段 residentHero 连接到 Hero 类型上。

接下来是 watchOS App 主视图的源代码:

struct ContentView: View {@Environment(\.scenePhase) var scenePhase@Environment(\.modelContext) var modelContextvar body: some View {NavigationStack {Group {// 具体实现从略...}.navigationTitle("英雄集合")}.onChange(of: scenePhase) {_, new inif new == .inactive {Model.shared.chooseResidentHero()           // 1WidgetCenter.shared.reloadAllTimelines()    // 2}}}
}

从上面的代码能够看到,当 App 切换至非活动状态(inactive)时我们做了两件事:

  1. 为 Model 随机选择一个驻场英雄,并将新的关系保存到持久存储中;
  2. 刷新 Widgets 时间线从而促使小组件界面的刷新;

最后,是我们 watchOS Widget 界面的源代码:

struct IncValueWidgetEntryView : View {var entry: Provider.Entryvar body: some View {VStack {if let residentHero = Model.shared.residentHero {VStack(alignment: .leading) {HStack {Label(residentHero.name, systemImage: "person.and.background.dotted").foregroundStyle(.red).minimumScaleFactor(0.5)Spacer()Text("已驻场 \(residentHero.residentCount) 次").font(.system(size: 12)).foregroundStyle(.secondary)}HStack {Text("战斗力 \(residentHero.power)").minimumScaleFactor(0.5)Spacer()Button(intent: EnhancePowerIntent()) {Image(systemName: "bolt.ring.closed")}.tint(.green)}}} else {ContentUnavailableView("英雄都放假了...", systemImage: "xmark.seal")}}.fontWeight(.heavy)}
}

可以看到当 Widget 的界面刷新后,我们尝试从共享 Model 实例的 residentHero 关系中读取出对应的驻场英雄,然后将其显示在小组件中。

在 Xcode 预览中差不多是这个样子滴:

在这里插入图片描述

然而,现在执行的结果是:App 明明更新了共享 Model 中的驻场英雄,但是 Widget 里却“涛声依旧”的显示“英雄都在放假”呢?

这样一个简单的代码逻辑却无法让我们得偿所愿,为什么呢?

2. 推本溯源

虽然上面代码简单的不要不要的,但其中有仍有几个关键“隐患”点在调试时需要排除:

  1. App 在进入后台前是否更新驻场英雄数据到持久存储上了?
  2. 在更新驻场英雄后是否确保 Widget 被及时刷新了?
  3. 刷新后的 Widget 是否可以确保与 App 共享同一个持久存储?

第一条很好排除,只需要在 App 对应的代码行上设置断点然后观察其执行结果即可。

第二条需要在 Widget 界面视图中设置断点,然后用调试器附着到小组件执行进程上观察即可。

经过测试可以彻底排除前两个潜在“故障点”。福尔摩斯曾经说过:“当你排除一切不可能的情况。剩下的,不管多难以置信,那都是事实

所以,问题的原因一定是 App 和 Widget 之间没有正确同步它们的底层数据。

回到共享 Model 静态属性的代码中,可以看到我们的 shared 属性其实是一个惰性(lazy)属性:

@MainActor
static var shared: Model = {let context = ModelContainer.auto.mainContextlet predicate = #Predicate<Model> { model inmodel.mid == UniqID}let desc = FetchDescriptor(predicate: predicate)if let result = try! context.fetch(desc).first {return result} else {let new = Model(mid: UniqID)context.insert(new)try! context.save()return new}
}()

这意味着:当它被求过值后,后续的访问不会再重新计算这个值了。

当我们在 Widget 里第一次访问它时,其 residentHero 关系字段中还未包含对应的驻场英雄。当 App 更新了驻场英雄后,Widget 中原来的 Model.shared 对象并不会自动刷新来反映持久存储中数据的改变。这就是问题的根本原因!

3. 解决之道

在了然了问题的根源之后,解决起来就是小菜一碟了。

最简单的方法,我们只需将原来的惰性属性变为计算属性即可。这样一来,我们即可确保在每次访问 Model 的共享单例时它的内容都会得到及时的刷新:

@MainActor
static var liveShared: Model {let context = ModelContainer.auto.mainContextlet predicate = #Predicate<Model> { model inmodel.mid == UniqID}let desc = FetchDescriptor(predicate: predicate)if let result = try! context.fetch(desc).first {return result} else {let new = Model(mid: UniqID)context.insert(new)try! context.save()return new}
}

如上代码所示,我们将之前的惰性属性变为了“活泼”的计算属性,这样 Widget 每次访问的 Model 共享实例都会是“最新鲜”的:

struct IncValueWidgetEntryView : View {var entry: Provider.Entryvar body: some View {VStack {if let residentHero = Model.liveShared.residentHero {// 原代码从略...} else {ContentUnavailableView("英雄都放假了...", systemImage: "xmark.seal")}}.fontWeight(.heavy)}
}

编译并再次运行 App,当切换至对应 Widget 后可以看到我们的驻场英雄闪亮登场啦:

在这里插入图片描述

至此,我们解决了博文开头那个问题,棒棒哒!💯

总结

在本篇博文中,我们讨论了 SwiftData 共享数据库在 App 中做出的改变,却无法被 对应 Widgets 感知的问题。我们随后找出了问题的原因并“一发入魂”将其完美解决。

感谢观赏,再会啦!😎


http://www.mrgr.cn/news/30947.html

相关文章:

  • 新160个crackme - 060-snake
  • 条件编译代码记录
  • Nomad Web服务终于成熟了!
  • 学习使用Docker
  • Tableau Einstein 重磅亮相,融合 AI 与数据云提供统一且无缝的分析新体验!
  • 需求3:照猫画虎
  • 第314题|参考!如何做到【一题多解】|武忠祥老师每日一题
  • Linux操作系统 进程(3)
  • 免密执行远程服务命令
  • Revit学习记录-版本2018【持续补充】
  • Streamlit:使用 Python 快速开发 Web 应用
  • 我的数据库旅程:从迷茫到觉醒
  • 1332. 删除回文子序列 脑筋急转弯
  • 《俄语翻译通》app一款专业的俄文OCR识别器,学俄语不会颤音怎么办?《俄语翻译通》可以帮助你!
  • Windows用管理员运行cmd命令后无法切换盘符
  • 23个Python在自然语言处理中的应用实例
  • TiDB 中的自增主键有哪些使用限制,应该如何避免?
  • HCL Domino 14.5EAP1快问快答
  • 解决Filament中使用ARCore出现绿色闪屏的问题
  • 力扣150题——多维动态规划