ToB企服应用市场:ToB评测及商务社交产业平台

标题: SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的缘故原由和办理 [打印本页]

作者: 曂沅仴駦    时间: 2024-10-21 04:07
标题: SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的缘故原由和办理

0. 问题现象

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

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

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

1. 示例代码

起首是 SwiftData 数据模型:
  1. import Foundation
  2. import SwiftData
  3. @Model
  4. class Hero {
  5.     var hid: UUID
  6.     var name: String
  7.     var power: Int
  8.     var residentCount: Int = 0
  9.     var timestamp: Date
  10.    
  11.     init(name: String, power: Int) {
  12.         self.hid = UUID()
  13.         self.name = name
  14.         self.power = power
  15.         timestamp = .now
  16.     }
  17.    
  18.     func update() {
  19.         timestamp = .now
  20.     }
  21.    
  22.     private static let HeroInfos: [(name: String, power: Int)] = [
  23.         ("黑悟空", 10000),
  24.         ("钢铁侠", 5000),
  25.         ("灭霸他爸", 500000),
  26.     ]
  27.    
  28.     @MainActor
  29.     static func spawnHeros(forPreview: Bool = true) {
  30.         let container = forPreview ? ModelContainer.preview : .shared
  31.         let context = container.mainContext
  32.         
  33.         if !forPreview {
  34.             let desc = FetchDescriptor<Hero>()
  35.             if try! context.fetchCount(desc) > 0 {
  36.                 return
  37.             }
  38.         }
  39.         
  40.         for hero in HeroInfos {
  41.             let new = Hero(name: hero.name, power: hero.power)
  42.             context.insert(new)
  43.         }
  44.         
  45.         try! context.save()
  46.     }
  47. }
  48. @Model
  49. class Model {
  50.     private static let UniqID = UUID(uuidString: "3788ABA9-043C-4D34-B119-5D69D486CBBA")!
  51.    
  52.     var mid: UUID
  53.    
  54.     @Relationship(deleteRule: .nullify)
  55.     var residentHero: Hero?
  56.    
  57.     init(mid: UUID) {
  58.         self.mid = mid
  59.         self.residentHero = nil
  60.     }
  61.    
  62.     @MainActor
  63.     static var shared: Model = {
  64.         let context = ModelContainer.auto.mainContext
  65.         let predicate = #Predicate<Model> { model in
  66.             model.mid == UniqID
  67.         }
  68.         
  69.         let desc = FetchDescriptor(predicate: predicate)
  70.         if let result = try! context.fetch(desc).first {
  71.             return result
  72.         } else {
  73.             let new = Model(mid: UniqID)
  74.             context.insert(new)
  75.             try! context.save()
  76.             return new
  77.         }
  78.     }()
  79.    
  80.     // 随机产生驻场英雄
  81.     @MainActor
  82.     func chooseResidentHero() {
  83.         let context = ModelContainer.auto.mainContext
  84.         let desc = FetchDescriptor<Hero>(sortBy: [.init(\Hero.power)])
  85.         
  86.         if let hero = try! context.fetch(desc).randomElement() {
  87.             residentHero = hero
  88.             hero.residentCount += 1
  89.             try! context.save()
  90.         }
  91.     }
  92. }
复制代码
可以看到,我们的 App 由 Hero 和 Model 两种数据模型构成。此中,在 Model 里我们以关系(@Relationship)的形式将驻场英雄字段 residentHero 毗连到 Hero 范例上。
接下来是 watchOS App 主视图的源代码:
  1. struct ContentView: View {
  2.    
  3.     @Environment(\.scenePhase) var scenePhase
  4.     @Environment(\.modelContext) var modelContext
  5.         
  6.     var body: some View {
  7.         NavigationStack {
  8.             Group {
  9.                 // 具体实现从略...
  10.             }
  11.             .navigationTitle("英雄集合")
  12.         }
  13.         .onChange(of: scenePhase) {_, new in
  14.             if new == .inactive {
  15.                 Model.shared.chooseResidentHero()           // 1
  16.                 WidgetCenter.shared.reloadAllTimelines()    // 2
  17.             }
  18.         }
  19.     }
  20. }
复制代码
从上面的代码可以或许看到,当 App 切换至非活动状态(inactive)时我们做了两件事:
最后,是我们 watchOS Widget 界面的源代码:
  1. struct IncValueWidgetEntryView : View {
  2.     var entry: Provider.Entry
  3.     var body: some View {
  4.         VStack {
  5.             if let residentHero = Model.shared.residentHero {
  6.                 VStack(alignment: .leading) {
  7.                     HStack {
  8.                         Label(residentHero.name, systemImage: "person.and.background.dotted")
  9.                             .foregroundStyle(.red)
  10.                             .minimumScaleFactor(0.5)
  11.                         Spacer()
  12.                         Text("已驻场 \(residentHero.residentCount) 次")
  13.                             .font(.system(size: 12))
  14.                             .foregroundStyle(.secondary)
  15.                     }
  16.                     
  17.                     HStack {
  18.                         Text("战斗力 \(residentHero.power)")
  19.                             .minimumScaleFactor(0.5)
  20.                         Spacer()
  21.                         Button(intent: EnhancePowerIntent()) {
  22.                             Image(systemName: "bolt.ring.closed")
  23.                         }
  24.                         .tint(.green)
  25.                     }
  26.                 }
  27.             } else {
  28.                 ContentUnavailableView("英雄都放假了...", systemImage: "xmark.seal")
  29.             }
  30.         }
  31.         .fontWeight(.heavy)
  32.     }
  33. }
复制代码
可以看到当 Widget 的界面革新后,我们尝试从共享 Model 实例的 residentHero 关系中读取出对应的驻场英雄,然后将其显示在小组件中。
在 Xcode 预览中差不多是这个样子滴:

然而,现在实行的结果是:App 明明更新了共享 Model 中的驻场英雄,但是 Widget 里却“涛声仍旧”的显示“英雄都在放假”呢?
这样一个简朴的代码逻辑却无法让我们得偿所愿,为什么呢?
2. 推本溯源

虽然上面代码简朴的不要不要的,但此中有仍有几个关键“隐患”点在调试时需要排除:
第一条很好排除,只需要在 App 对应的代码行上设置断点然后观察其实行结果即可。
第二条需要在 Widget 界面视图中设置断点,然后用调试器附着到小组件实行进程上观察即可。
颠末测试可以彻底排除前两个埋伏“故障点”。福尔摩斯曾经说过:“当你排除一切不可能的情况。剩下的,不管多难以置信,那都是事实
以是,问题的缘故原由一定是 App 和 Widget 之间没有精确同步它们的底层数据。
回到共享 Model 静态属性的代码中,可以看到我们的 shared 属性其实是一个惰性(lazy)属性:
  1. @MainActor
  2. static var shared: Model = {
  3.     let context = ModelContainer.auto.mainContext
  4.     let predicate = #Predicate<Model> { model in
  5.         model.mid == UniqID
  6.     }
  7.    
  8.     let desc = FetchDescriptor(predicate: predicate)
  9.     if let result = try! context.fetch(desc).first {
  10.         return result
  11.     } else {
  12.         let new = Model(mid: UniqID)
  13.         context.insert(new)
  14.         try! context.save()
  15.         return new
  16.     }
  17. }()
复制代码
这意味着:当它被求过值后,后续的访问不会再重新计算这个值了。
当我们在 Widget 里第一次访问它时,其 residentHero 关系字段中还未包罗对应的驻场英雄。当 App 更新了驻场英雄后,Widget 中原来的 Model.shared 对象并不会自动革新来反映持久存储中数据的改变。这就是问题的根本缘故原由!
3. 办理之道

在了然了问题的根源之后,办理起来就是小菜一碟了。
最简朴的方法,我们只需将原来的惰性属性变为计算属性即可。这样一来,我们即可确保在每次访问 Model 的共享单例时它的内容都会得到实时的革新:
  1. @MainActor
  2. static var liveShared: Model {
  3.     let context = ModelContainer.auto.mainContext
  4.     let predicate = #Predicate<Model> { model in
  5.         model.mid == UniqID
  6.     }
  7.    
  8.     let desc = FetchDescriptor(predicate: predicate)
  9.     if let result = try! context.fetch(desc).first {
  10.         return result
  11.     } else {
  12.         let new = Model(mid: UniqID)
  13.         context.insert(new)
  14.         try! context.save()
  15.         return new
  16.     }
  17. }
复制代码
如上代码所示,我们将之前的惰性属性变为了“生动”的计算属性,这样 Widget 每次访问的 Model 共享实例都会是“最新鲜”的:
  1. struct IncValueWidgetEntryView : View {
  2.     var entry: Provider.Entry
  3.     var body: some View {
  4.         VStack {
  5.             if let residentHero = Model.liveShared.residentHero {
  6.                 // 原代码从略...
  7.             } else {
  8.                 ContentUnavailableView("英雄都放假了...", systemImage: "xmark.seal")
  9.             }
  10.         }
  11.         .fontWeight(.heavy)
  12.     }
  13. }
复制代码
编译并再次运行 App,当切换至对应 Widget 后可以看到我们的驻场英雄闪亮登场啦:

至此,我们办理了博文开头那个问题,棒棒哒!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4