工程实践

云端 Mac 上验收 iOS WidgetKit 时间线与共享数据

云端 Mac 上验收 iOS WidgetKit 时间线与共享数据

团队把待办数量写进主应用后,远程模拟器上的小组件仍显示旧值。此时直接调用更多次刷新接口,通常只会增加排查噪声:写入共享数据、扩展读取数据、生成时间线和系统安排显示,是四个不同环节。下面以一个显示待办数量的 WidgetKit 小组件为例,把每个环节变成可单独验收的条件。

先画清数据交接边界

主应用与小组件扩展不是同一个进程。普通的 UserDefaults.standard 不会自动成为双方共用的数据源。先在工程的两个 target 上启用同一个 App Group,并把组标识放进双方都能读取的工程配置;不要把测试用标识硬编码成发布配置。

主应用完成业务数据写入后,向 UserDefaults(suiteName:) 保存数量和更新时间。扩展使用相同的 suite 名读取。写入函数应返回成功或错误,而不是静默采用旧值;扩展读取失败时则展示明确的默认状态。若数据结构不止一个整数,将它编码为一份带版本号的记录,避免数量已更新、标题仍是旧值的跨字段组合。

刷新请求只是在通知系统“数据可能变了”;它既不负责把数据从主应用搬进扩展,也不保证小组件立即重绘。

先在主应用写入后读回一次,再在扩展读取处记录脱敏后的版本号。两端版本不同,问题在数据交接;版本相同但画面不同,才继续看时间线与显示。不要把任务内容、账户信息或完整共享记录写进诊断日志。

把时间线计算变成纯函数

时间线的条目内容不应依赖测试执行的真实时钟。让提供器只负责读取数据和调用计算函数,计算函数接收当前时间与已读取的快照。下面的代码展示最小边界;类型可放在主应用与扩展共用的源文件中。

import Foundation

struct CountEntry: Equatable {
    let date: Date
    let count: Int
    let revision: Int
}

func makeEntries(
    now: Date,
    count: Int,
    revision: Int
) -> [CountEntry] {
    [
        CountEntry(date: now, count: count, revision: revision),
        CountEntry(
            date: now.addingTimeInterval(15 * 60),
            count: count,
            revision: revision
        )
    ]
}

第二个条目仍携带同一份快照,不能凭经过十五分钟就推断待办数量会变化。提供器取得条目后,可以为时间线设置晚于末条目的重新请求策略;策略指定的是期望的检查时机,不是界面更新的秒级承诺。收到新的业务数据时,再请求对应种类的小组件重新加载。若产品要求显示精确倒计时,应重新审视需求,而不是靠密集的时间线条目模拟实时进程。

固定输入,断言可控结果

在单元测试中构造固定 Date、数量和修订号,断言恰好生成两个条目、日期严格递增,并且两个条目的数量与修订号一致。再测一次数量为零的边界:零应被渲染为真实值,不能被读取失败时的默认值掩盖。这样的测试不启动模拟器,也不等待系统排程,因此失败时能直接定位计算逻辑。

在云端模拟器验证真实交接

先用 xcrun simctl list devices available 找到可用的 iOS 模拟器及其设备 ID。构建与测试时固定目标 ID,避免执行器换机后按设备名称匹配到另一套运行时:

xcodebuild test \
  -project Widgets.xcodeproj \
  -scheme Widgets \
  -destination "platform=iOS Simulator,id=$SIM_ID" \
  -resultBundlePath "$PWD/widget-tests.xcresult"

这里的工程名和 scheme 是示例,执行前换成仓库中真实存在的名称;SIM_ID 应由任务配置明确传入。每次执行使用新的结果目录,避免已有 .xcresult 使命令提前失败。时间线单元测试通过后,再安装包含主应用与扩展的构建,在模拟器中添加小组件:先确认默认状态,再从主应用写入一条新修订并请求刷新,最后核对扩展读取记录与画面。

界面验收只检查最终内容及修订号,不把“请求后若干秒内出现”设为硬断言。系统可以调整刷新时机;若测试需要精确验证即时显示,应测试主应用内可控的预览视图,而不是将系统调度误当作应用定时器。

按症状定位,而不是反复重装

观察结果 优先检查
主应用读回的值就不对 写入时机、suite 名和错误处理
主应用正确,扩展仍读旧值 两个 target 的 App Group 配置及记录版本
扩展读到新值,条目仍旧 提供器传参和时间线构造
新条目正确,画面仍旧 小组件种类、视图取值及系统实际显示时机

排查前先记下工程提交、运行时版本、设备 ID 和共享记录修订号;它们比一张“画面没变”的截图更能说明问题。不要在每次失败后立即擦除整个模拟器,那会连同可复现的共享数据一起删掉。需要重置时,先保留测试结果与必要的脱敏记录,再明确选择清除应用数据或重建模拟器。

把验收条件交给流水线

自动化任务分成两层:第一层运行纯函数测试,阻断条目顺序、零值或修订号回归;第二层在固定模拟器上检查应用与扩展均已安装、双方读取同一份记录,并人工或通过可控预览核对布局。小组件的实际刷新另列为观察项,不和第一层的确定性断言混在一起。

最后检查三件事:两个 target 是否指向同一个共享组,新增数据是否带可追踪但不泄露内容的修订号,测试是否把时间作为输入而非等待真实分钟流逝。这三项成立后,下一次出现旧画面,就能沿数据、条目、显示的顺序定位,而不用靠重复请求刷新猜测原因。

常见问题

调用刷新接口后,小组件为什么没有立即更新?

刷新请求不等于立即执行。先确认共享容器中已有新值、提供器能读到该值,再在模拟器上观察系统实际生成的时间线;不要用固定秒数断言界面必然变化。

时间线测试需要依赖模拟器等待刷新吗?

不需要。把当前时间和数据作为参数传给纯函数,直接断言条目的时间顺序与内容;模拟器测试只负责核对安装、数据交接及最终显示。

独享 Apple Silicon

把固定配置的云端 Mac 接入你的工作流

按日、周、月或季租用独享物理节点,使用固定硬件资源运行构建、自动化和 MLX 推理任务。

查看可租用配置