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

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

时间:2024-09-20 09:51:05浏览次数:11  
标签:驻场 App var SwiftData let context Widgets Model

在这里插入图片描述

0. 问题现象

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

在这里插入图片描述

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

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

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


1. 示例代码

首先是 SwiftData 数据模型:

import Foundation
import SwiftData

@Model
class Hero {
    var hid: UUID
    var name: String
    var power: Int
    var residentCount: Int = 0
    var timestamp: Date
    
    init(name: String, power: Int) {
        self.hid = UUID()
        self.name = name
        self.power = power
        timestamp = .now
    }
    
    func update() {
        timestamp = .now
    }
    
    private static let HeroInfos: [(name: String, power: Int)] = [
        ("黑悟空", 10000),
        ("钢铁侠", 5000),
        ("灭霸他爸", 500000),
    ]
    
    @MainActor
    static func spawnHeros(forPreview: Bool = true) {
        let container = forPreview ? ModelContainer.preview : .shared
        let context = container.mainContext
        
        if !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 = mid
        self.residentHero = nil
    }
    
    @MainActor
    static var shared: Model = {
        let context = ModelContainer.auto.mainContext
        let predicate = #Predicate<Model> { model in
            model.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
        }
    }()
    
    // 随机产生驻场英雄
    @MainActor
    func chooseResidentHero() {
        let context = ModelContainer.auto.mainContext
        let desc = FetchDescriptor<Hero>(sortBy: [.init(\Hero.power)])
        
        if let hero = try! context.fetch(desc).randomElement() {
            residentHero = hero
            hero.residentCount += 1
            try! context.save()
        }
    }
}

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

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

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

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

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

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

struct IncValueWidgetEntryView : View {
    var entry: Provider.Entry

    var 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.mainContext
    let predicate = #Predicate<Model> { model in
        model.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.mainContext
    let predicate = #Predicate<Model> { model in
        model.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.Entry

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

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

在这里插入图片描述

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

标签:驻场,App,var,SwiftData,let,context,Widgets,Model
From: https://blog.csdn.net/mydo/article/details/141917654

相关文章

  • uniapp 封装请求方法
    目录uni.request()封装uni.showLoading()封装使用request() 方法uni.request()封装//request.jsconstBASE_URL='https://tea.qingnian8.com/api';//请求函数exportconstrequest=(option={})=>{ //解构并赋初始值 let{ url, data={}, ......
  • java计算机毕业设计少儿英语在线学习平台APP(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着全球化进程的加速和互联网技术的飞速发展,英语作为国际交流的重要工具,其教育需求在少儿群体中日益凸显。传统线下英语教育模式受限于时间、地点及......
  • 基于LangChain手工测试用例转App自动化测试生成工具
    在传统编写App自动化测试用例的过程中,基本都是需要测试工程师,根据功能测试用例转换为自动化测试的用例。市面上自动生成Web或App自动化测试用例的产品无非也都是通过录制的方式,获取操作人的行为操作,从而记录测试用例。整个过程类似于但是通常录制出来的用例可用性、可维护性......
  • 基于LangChain手工测试用例转App自动化测试生成工具
    在传统编写App自动化测试用例的过程中,基本都是需要测试工程师,根据功能测试用例转换为自动化测试的用例。市面上自动生成Web或App自动化测试用例的产品无非也都是通过录制的方式,获取操作人的行为操作,从而记录测试用例。整个过程类似于但是通常录制出来的用例可用性、可维护性......
  • 短剧平台搭建小程序APP开发
    短剧平台搭建小程序APP开发(张先生:13101716752微电)出海短剧项目设计丶短剧系统APP源码以下是关于短剧平台搭建(小程序和APP开发)的一些要点:一、需求分析用户端需求短剧观看体验提供流畅的视频播放功能,支持多种视频格式,确保不同短剧在各种网络条件下都能正常播放,具有高清画......
  • 基于LangChain手工测试用例转App自动化测试生成工具
    在传统编写App自动化测试用例的过程中,基本都是需要测试工程师,根据功能测试用例转换为自动化测试的用例。市面上自动生成Web或App自动化测试用例的产品无非也都是通过录制的方式,获取操作人的行为操作,从而记录测试用例。整个过程类似于但是通常录制出来的用例可用性、可维护......
  • 预约问诊APP开发指南:基于互联网医院系统源码的实践方案
    本篇文章,小编将深入探讨如何基于互联网医院系统源码开发预约问诊APP,帮助开发者更好地理解实践中的关键环节与技术方案。 一、互联网医院系统源码的核心功能在开发预约问诊APP之前,理解互联网医院系统源码的核心功能是第一步。通常,成熟的互联网医院系统源码包含以下几个模块:-用户管......
  • happiness(栈)
    happiness(栈)//happiness#include<stdio.h>#include<stdlib.h>#defineMAX_N100000//函数声明intmax_happiness(intn,intw[]);intmain(){ intn; //输入物品数量 scanf("%d",&n); //输入每个物品的满意度 intw[MAX_N]; for(inti=......
  • 易优eyoucms网站报错 \core\library\think\App.php Fatal error: Call to undefin
    当你遇到 Fatalerror:Calltoundefinedfunctionthink\switch_citysite() 这样的错误时,说明在代码中调用了一个未定义的函数 think\switch_citysite()。这种情况通常是因为函数没有被正确地引入或者该函数根本不存在于当前的代码库中。解决方案确认函数的存在检查 s......
  • Uniapp生命周期
    UniApp框架中的生命周期函数主要分为两大类:页面生命周期和组件生命周期。页面生命周期:onLoad:页面加载时触发。onShow:页面显示时触发。onReady:页面初次渲染完成时触发。onHide:页面隐藏时触发。onUnload:页面卸载时触发。onPullDownRefresh:用户......