Skip to content

feat(swift): iOS 示例应用与模拟器 e2e(本地)——HealthKit 授权归属应用、实例、前后台;修复监听器与通道的生命周期缺陷(#389) - #509

Merged
eric8810 merged 13 commits into
mainfrom
feat/swift-app-ios
Oct 21, 2026

Conversation

@eric8810

@eric8810 eric8810 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

#389(Swift A1:应用托管)的第二部分,接 #503:iOS 示例应用、HealthKit 插件、模拟器上的 e2e,SDK 自己的测试在 iOS 模拟器上运行(都只在本地),以及由此发现的两个 SDK 缺陷的修复与回归测试。基于 #503 的分支,#503 合入后 diff 只剩本 PR 的提交。

示例应用 swift/examples/HealthApp

  • Xcode 项目(SDK 作本地包 ../../..,Info.plist 由构建设置生成:显示名、NSHealthShareUsageDescription / NSHealthUpdateUsageDescription、NSLocalNetworkUsageDescription;HealthKit entitlement)。真机只需在 Signing 里选 team。
  • 插件就是普通的 Swift 插件,不声明 provides(开发者无感;拨入连接里没声明的服务由 fix(loader): 连进来的运行时没有声明就提供的服务,本实例里的行用不到 #506 起能被同一实例的行使用):health(HealthKit:status、authorize、stepsToday,未授权时抛 HealthAccessDenied,带 code)、device(id、model、system——rutis 不建模设备,由插件提供)。
  • 业务层:scenePhase 为 .active 时 start(),.background 时在后台任务里 stop();界面显示 rutis.state;连接参数来自启动参数(-RutisConnect、-RutisID、-RutisToken、-RutisCA)。

模拟器 e2e:cargo test -p rutis-e2e --test ios_app -- --ignored(只在本地)

用 xcodebuild 构建示例,新建两台模拟器(sim-a、sim-b),rutis-host run 带实例分组(phone:health、phone:device 与一个 Python 行 coach),检查:

验收 怎样检查
应用拨出接入,按 rutis 配置装载插件 两台各自连上(rutis: connected to … as sim-a),coach 在各自实例里运行
两台设备同名插件各在自己的实例 每个 coach 读到的 device.id() 是自己那台
stop 后使用者停下、start 后恢复 把"设置"切到前台让 sim-a 进入后台:业务层 stop(),sim-a 的 coach 停下、sim-b 的照常;切回前台:start(),coach 恢复;应用日志 app: state stopped / connected
拒绝后错误传回 HealthKit 未决定时 stepsToday() 在 Python 调用方失败:HealthAccessDenied: step counts are not shared with this app (not-determined),name、code 都传到
授权提示指向应用 应用在前台后请求授权,"健康"授权页显示应用名与它的用途说明(截图经 Vision 读出,tests/e2e/ios/text.swift);应用结束后等待中的调用结束

本地运行(rebase 到 main 之后):Xcode 26.5、iOS 26.5 模拟器,ios_app 通过(约 100 秒),bash tools/ios-selftest.sh 34 项全过。此前在 runner 的 iOS 27.1 模拟器上也通过过一次。iOS 27 上应用尚未激活时请求 HealthKit 授权会超时(Authorization session timed out),所以 e2e 改为应用在前台之后才触发请求。

按用户的决定,需要模拟器、设备或系统授权提示的测试不在 CI 里跑:本 PR 不加 CI job,docs/ci.md §12「本地测试」加 iOS 一行(bash tools/ios-selftest.sh、cargo test -p rutis-e2e --test ios_app -- --ignored)。

模拟器上做不到的:点击授权页(simctl privacy 没有 HealthKit 服务,Xcode 26.5)。所以"拒绝"(code: denied)、"授权后返回步数"以及一切真机项写进指南 §8 的人工清单。手动在模拟器上看过授权页(中文系统):标题"访问健康数据"、"Rutis Health 想要访问并更新你的健康数据"、App 说明为 NSHealthUpdateUsageDescription 的原文。

iOS 模拟器测试发现的 SDK 缺陷(macOS 与 iOS 共用代码,已修复)

  1. unowned 引用在释放后被读:iOS 模拟器上的自测偶发 Fatal error: Attempted to read an unowned reference but object … was already destroyed。原因是排进监听器队列的调用在测试宿主或运行时释放之后才执行。LocalListener 对运行时、RutisTest 的 BusEntry 对测试宿主改为 weak:它们释放后,排队的调用什么也不启动。会话的 dispatch / arrive 闭包也改为 weak,对端已不在时以错误答复。
  2. 关闭的通道碰到被复用的描述符:LineChannel 在两次读取之间被关闭时,描述符已经释放。之后的 receive 仍会 poll / read 那个号码(这时它可能已是新会话的 socket),读走新会话的问候,新会话就挂住;close 的 shutdown 也可能落在被复用的号码上。现在关闭后 receive 返回 nil、send 报错,shutdown 在锁内进行,释放等到进行中的写完成之后。
  3. 回归测试 LifetimeTests,在 swift run RutisSelfTest 里,CI 的 runtimes-swift 在 macOS 上跑:
    • 旧代码上,排队调用的用例崩溃(unowned),关闭通道的用例挂住(描述符复用);
    • 新代码在 macOS 上连跑 30 次全过,在 iOS 模拟器上也通过。

文档

  • 指南 docs/guide/swift-app.md(中英):§2 改为不声明 provides;加 §8 示例应用(写明只在本地运行)与真机人工验收清单(本地网络权限的允许 / 拒绝、提示归属、拒绝与授权、后台与前台、两台设备、断网)。
  • 设计 §17.5:补示例、e2e、只在本地运行的原因。
  • 承诺登记:新增 swift/app-access-belongs-to-the-app(部分验证:拒绝与真机为人工,测试只在本地);swift/app-start-stop 加模拟器测试。
  • docs/ci §12 加 iOS 一行。

Closes #389

…389)

- swift/examples/HealthApp:Xcode 项目,SDK 作本地包;health(HealthKit)与 device 插件;业务层前台 start、进入后台 stop;连接参数取自启动参数
- tests/e2e/tests/ios_app.rs(--ignored):两台新建模拟器各自拨入 rutis-host,两个实例、各自的 Python 行用自己应用的 device;进入后台行停下、回到前台恢复,另一台不受影响;HealthKit 未决定时插件的 HealthAccessDenied(code not-determined)传到 Python 调用方;请求授权时"健康"授权页显示应用名与用途说明(截图经 Vision 读出)
- tools/ios-selftest.sh:RutisSelfTest 为 iOS 模拟器构建、simctl spawn 运行
- CI:job ios(完整运行、改动示例或其测试的 PR);承诺 swift/app-access-belongs-to-the-app;指南 §8 示例与真机人工清单;设计 §17.5
@eric8810 eric8810 added this to the 0.15 milestone Oct 20, 2026
iOS 模拟器上的 SDK 自测偶发 "Attempted to read an unowned reference but object was already destroyed":emit 排进监听器队列的调用在测试关闭、运行时释放之后才执行,LocalListener 的 unowned runtime 已失效。监听器改为持有运行时(撤回监听器——每次卸载都会——解开引用);会话的 dispatch / arrive 闭包改为 weak,运行时不在时以错误答复。RutisTest 的 BusEntry 与 harness 同样处理。
- LocalListener、RutisTest 的 BusEntry 对运行时 / 测试宿主改为 weak(排队的调用在它们释放后什么也不启动),会话的 dispatch / arrive 闭包为 weak;
- LineChannel:关闭后(两次读取之间)receive 返回 nil、send 报错,shutdown 在锁内进行、释放在写完之后——之前关闭的通道会读走或关掉同号的新 socket,新会话收不到问候而挂住;
- SelfTest 加 LifetimeTests(macOS 与 iOS 共用同一路径,runtimes-swift 的 swift run RutisSelfTest 即覆盖):旧代码分别崩溃(unowned)与挂住(描述符复用),新代码 macOS 连跑 30 次全过
- ios_app e2e:授权在应用已在前台后由 <id>.authorize 触发(iOS 27 上应用未激活时 HealthKit 授权会超时)
@eric8810 eric8810 changed the title feat(swift): iOS 示例应用与模拟器 e2e——HealthKit 授权归属应用、实例、前后台(#389) feat(swift): iOS 示例应用与模拟器 e2e(本地)——HealthKit 授权归属应用、实例、前后台;修复监听器与通道的生命周期缺陷(#389) Oct 21, 2026
@eric8810
eric8810 merged commit e6e291c into main Oct 21, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Swift A1:应用托管(RutisApp,监听与拨出)

1 participant