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.xdnf.cn/news/144650.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql事务相关

什么是事务 事务是指是程序中一系列严密的逻辑操作&#xff0c;而且所有操作必须全部成功完成&#xff0c;否则在每个操作中所作的所有更改都会被撤消。可以通俗理解为&#xff1a;就是把多件事情当做一件事情来处理&#xff0c;好比大家同在一条船上&#xff0c;要活一起活&a…

置换排列的数学表达与Benes网络

摘要 本文主要讨论如何使用Benes网络完成排列的置换操作&#xff0c;介绍Benes网络的构造&#xff0c;以及具体的路由方式。 置换排列 这里的排列指一个n个不同元素的序列&#xff0c;不同的顺序代表不同的排列。比如 [ 1 , 2 , 3 , 4 ] [1,2,3,4] [1,2,3,4]和 [ 2 , 1 , 4 …

优青博导团队提供生信分析整体解决方案丨组学技术服务、表观组分析、互作组分析、遗传转化实验、单细胞检测与生物医学

平民价格给您最优的技术服务——生物信息技术分析就找汇智生物 业务领域&#xff1a; 组学技术服务 、表观组分析、互作组分析、遗传转化实验、单细胞检测与生物医学 合作免费提供&#xff1a; 博导团队免费指导实验、免费解读实验结果、实验整体打包服务、免费论文润色 生物…

TAPD7.0焕新升级!助力企业数字化敏捷研发提效

近日&#xff0c;TAPD的7.0升级版本&#xff0c;不仅外观、引擎、协作焕新升级&#xff0c;大型产品规模化&#xff0c;敏捷‍‍‍‍‍‍‍‍更跨组织/地域&#xff0c;研发协作小团队更轻便。 腾讯TAPD7.0焕新升级&#xff01; “外观”升级 导航革新&#xff1a;重塑导航栏…

windows10部署ChatTTS+Apifox调用

1 文件准备 准备好如下图所示的文件 2 修改ChatTTS_Win\ChatTTS\uilib\cfg.py 如下图所示&#xff0c;注释第34行&#xff0c;增加 WEB_ADDRESS 0.0.0.0:9998确保局域网内的其他设备也可以请求该服务。 3 启动服务 4 发送post请求 对应的请求内容如下&#xff1a; bash代…

字节推音乐生成神器 Seed-Music 支持多样化输入和精确控制

Seed-Music&#xff0c;字节跳动的最新音乐创作神器&#xff0c;能通过文字、音频等多种方式轻松生成音乐&#xff0c;仿佛拥有魔法般的魔力。它巧妙地融合了自回归语言模型与扩散模型&#xff0c;不仅确保了音乐作品的高品质&#xff0c;还赋予了用户对音乐细节的精准掌控能力…

【2025】中医药健康管理小程序(安卓原生开发+用户+管理员)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

电商商品详情API接口对电商跨境电商企业运营的好处

为了获取更大利益&#xff0c;电商商家经常需要使用价格&#xff0c;ERP接口系统。价格接口对电商商家有多方面的好处&#xff0c;主要体现在以下几个方面&#xff1a; 1、价格接口系统可以帮助品牌和商家实现更加科学和精准的定价策略。通过实时获取多个主流电商平台&#xf…

基于SpringBoot的智能排课系统设计与实现

文未可获取一份本项目的java源码和数据库参考。 &#xff08;一&#xff09;选题来源与背景 高校的每学期伊始&#xff0c;排课是教务处工作中的重中之重。安排合理无资源冲突&#xff08;教师、教室和设备等教学资源&#xff09;的课表是教务工作必须面临的问题。传统的人工…

【Python】练习:控制语句(二)第1关

第1关&#xff1a;分支结构基础实训 第一题第二题第三题第四题&#xff08;※&#xff09;第五题&#xff08;※&#xff09;第六题第七题 第一题 #第一题 for temp in [-280, -100, 0, 20, 120, 200]:#请在下面编写代码# ********** Begin ********** #if temp>-273.15:F9/…

使用Rust直接编译单个的Solidity合约

这里写自定义目录标题 使用Rust直接编译单个的Solidity合约前言预备知识准备工作示例 使用Rust直接编译单个的Solidity合约 前言 我们知道&#xff0c;我们平常开发Solidity智能合约时一般使用Hardhat框架&#xff0c;但是如果你是一个Rustacean (这是由 “Rust” 和 “crust…

SpringBoot项目同时集成Mybatis和Mybatis-plus框架

1. 背景 Mybatis-plus可以生成CRUD&#xff0c;减少开发中SQL编写量&#xff0c;但是某些情况下我们需要多表关联查询&#xff0c;这时候Mybatis可以手写SQL的优势就体现出来了&#xff0c;在实际开发中&#xff0c;项目里面一般都是Mybatis和Mybatis-Plus公用&#xff0c;但是…

<<编码>> 第 14 章 反馈与触发器(8)--带预设和清零的触发器 示例电路

带预设和清零的边缘触发 D 型触发器 info::操作说明 将 “清零” 置为高电平可将 Q 置为 0; 将 “预设” 置为高电平可将 Q 置为 1; 注: 如果两者同为高电平, 则清零占优, 应避免同时出现 其余操作同上 primary::在线交互操作链接 https://cc.xiaogd.net/?startCircuitLinkhtt…

0基础跟德姆(dom)一起学AI 数据处理和统计分析04-Panda入门

* Pandas数据结构介绍 * Series对象 * DataFrame对象 * Series常见操作 * 常用属性 * 常用方法 * 布尔索引 * 运算 * DataFrame常见操作 * 常用属性 * 常用方法 * 布尔索引 * 运算 * 更改操作 --- 1.Pandas数据结构介绍 * 图解 * 解释 * **DataFrame…

STM32最小系统核心板-SZPT领跑团队-C4

目录 一、团队介绍 队伍介绍 二、stm32f103c8t6构成 (1)概要 &#xff08;2&#xff09;构成 三、电路设计 &#xff08;1&#xff09;电源电路 &#xff08;2&#xff09;晶振电路 &#xff08;3&#xff09;SWD接口电路 &#xff08;4&#xff09;.复位电…

[Leetcode 543][Easy]-二叉树的直径-递归

目录 一、题目描述 二、整体思路 三、代码 一、题目描述 原题地址 二、整体思路 取一个结点的最大直径就是取一个结点的左子树最大深度右子树最大深度之和&#xff0c;因此可以定义一个递归函数&#xff0c;作用是取一个结点的最大直径。这个函数中还实现了求左子树最大深度…

Find My资讯|AirPods 4标准版充电盒无扬声器,Find My查找不会发出声音

苹果 AirPods 4 国行版标准版 999 元&#xff0c;主动降噪款 1399 元。标准版充电盒未内置扬声器&#xff0c;降噪版内置扬声器可用于查找功能。 苹果 AirPods 4 搭载 H2 芯片&#xff0c;引入计算音频技术&#xff0c;充电盒支持 USB-C 充电&#xff0c;体积比前代缩小 10% 以…

yolo车位数据集

停车场车位检测数据集是一个非常有价值的数据资源&#xff0c;它对于开发和训练能够自动识别停车位是否被占用的计算机视觉系统至关重要。以下是对这样一个数据集的详细介绍&#xff0c;以及如何使用这个数据集来训练YOLO&#xff08;You Only Look Once&#xff09;这样的目标…

nvm安装实现node多版本的切换

nvm安装实现node多版本的切换 方式一 下载安装包安装下载安装包解压安装设置 nvm 环境变量查看 nvm 是否安装完成安装 node 环境切换 node 版本列出已经安装的版本 方式二 一键脚本安装下载安装查看 nvm 是否安装完成安装 node 环境切换 node 版本列出已经安装的版本nvm相关命令…

基于yolov5的不同颜色安全帽检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv5的不同颜色安全帽检测系统是一种利用深度学习技术&#xff0c;特别是YOLOv5目标检测算法的创新应用。该系统旨在提高施工现场的安全管理水平&#xff0c;通过实时识别和检测工人佩戴的安全帽颜色&#xff0c;实现对安全规范的精准监督。 YOLOv5作为一…