iOS 技术案例
Elemental
一款私密的补剂与用药记录 App,把你实际服用的内容与自身健康趋势的变化对应起来,全部处理都在端上完成。
版本 1.0 iPhone、iOS 26+ 已于 2026 年 9 月提交 App Store 审核
- 角色
- 独立完成——产品设计、iOS 工程、发布
- 技术栈
- Swift, SwiftUI, GRDB/SQLite, WidgetKit, App Intents, HealthKit, Vision
- 架构
- SwiftUI App 层之下的平台中立领域包
- 网络暴露面
- 已发布 App 中为零——没有账号,没有服务器,没有分析 SDK
01塑造整个构建过程的约束
只有当记录者信任数据的去向时,健康记录才值得坚持记下去。补剂与用药日常恰恰是人们不愿交给账号系统的那类记录,因此在这里,隐私是一项架构属性,而不是隐私政策文件里的一句承诺。
这个决定会层层传导。既然没有服务器可以承接识别,标签解析就必须在端上运行。既然没有后端来计算统计,关联分析就必须是纯粹的、可测试的领域逻辑,在本机、并在后台任务中运行。既然没有分析 SDK,关于行为的问题就只能靠阅读代码与测试来回答,而不是看仪表盘。
最终得到的是一款刻意保持小表面积的已发布 App:本地存储、端上识别、只读的 Apple Health 访问,以及完全没有网络请求。
02它能做什么
四条根路径。Today 解析下一次计划摄入与当前批次,并记录已服用或已跳过。Products 通过拍摄标签建立产品档案。Ledger 保存实际摄入历史与结果记录。Insights 呈现已确认的摄入变化与其后发生的情况之间观察到的关联。
03架构
领域逻辑位于 ElementalCore,一个按限界上下文组织的平台中立 Swift Package:本体、供应、排程、摄入、剂量、检验、结果、分析与参考数据。核心模型是值类型且是 Sendable,因此同一套逻辑可以在 App、小组件扩展与测试中运行,无需模拟器。
SwiftUI 层按用户旅程而不是按屏幕类型组织,并与小组件扩展共用一个设计系统模块。小组件与 App Intents 通过主 App 使用的同一个解析器解析下一次剂量,并经由同一条幂等路径写入——从主屏幕发起的一次打卡不可能把同一条计划条目重复记录。
持久化是在 App Group 容器中使用 GRDB/SQLite,App、小组件与 App Intents 都会打开同一个容器。结构变更通过显式迁移完成;由早期私有容器版本创建的数据库会迁移进共享容器,而不会覆盖其中已有的数据。
两个值得特别说明的决定
- 历史在构造上只允许追加。排程以不可变、带生效日期的修订形式存储。编辑计划会创建一条从当天起往前生效的新修订;它永远不会改写日志里上个月已发生的事实。
- 「已跳过」不是「已服用」。处置与摄入事件分开存储,因此一次被跳过的剂量永远不会被聚合进摄入暴露,也不会被悄悄计作一次摄入。
04从一张标签照片到产品档案
读取一张 Supplement Facts 标签是端上抽取的一个很好的测试用例,因为答错的代价很高,而信息源本身又很杂乱:字号极小、单位噪声、未披露的复方成分,以及掺在事实里的营销文案。
- 拍摄相机或照片图库。图像只在内存中用于识别,不会另存为独立副本。
- 识别Vision OCR 在端上抽取文本;当设备支持时,可选使用 Foundation Models 增强。
- 结构化基于规则的解析把文本转换为具名成分、数量与单位,而不是把原始字符串继续往下传。
- 归一化条目会与内置目录中的同义词和常见剂量范围进行匹配。
- 拒绝不可信的内容低置信度、未知成分、单位异常、不可能的数量、超出范围的用量以及未披露内容,都会在保存前被标记出来。
- 确认用户复核并修改解析出的事实。识别负责提议,由人来做决定。任何未经确认的内容都不会被保存。
为什么这个顺序重要。真正值得关注的失效模式不是一次识别错误,而是一次识别错误被悄悄保存下来,污染此后数月的数据分析。把解析器当作顾问,并设置一道明确的复核关卡,才能保护后续分析所依赖的数据。
05从日志到观察
分析只问一个很窄的问题:当实际记录到的摄入发生变化并得到确认时,其前后的结果序列是否也随之出现了变化?设计上的每一个取舍,都是为了让答案不被过度断言。
- 摄入暴露由记录构建,而不是由计划构建。变化检测只依据已确认的摄入事件,对开始、中断、停用、剂量变化、频次变化与时间变化进行分类。
- 先过滤噪声,再做统计。三天窗口内的变化会合并为同一个转变标识,而小幅剂量漂移、时区偏移与七日重复候选会被抑制。
- 两样本窗口,有条件的结论。前后窗口按指标逐一比较,只有同时满足区间、效应量与阈值条件的结果才会被返回。
- 每条洞察都自带限定条件。每条洞察都会公开自己的样本窗口与局限,文案也会区分「仍在持续观察」与「已达条件的结论」。
- 措辞受到刻意约束。洞察描述的是关联、模式与趋势,不允许声称某种补剂或药物导致了某项变化。
领域层用纯粹的单元测试覆盖排程解析、摄入暴露变化检测、两样本估计与结果关联,这正是统计行为无需设备即可被审查的原因。
06决定了设计的正确性细节
领域中有若干部分很小、不起眼,却承担着关键作用。它们之所以存在,都是因为天真的实现会给出一个看起来合理、实则错误的数字:
- 剂量质量使用十进制定点运算,而不是二进制浮点,因此单位换算不会累积表示误差。
- 单位带类型并显式换算;毫克与微克是不同的量,永远不会合并。
- 负质量在构造时即被拒绝,因为负数量在聚合时会悄悄抵消总摄入暴露。
- 同日且单位兼容的记录会相加;单位不兼容的记录永远不会合并成同一行摄入暴露。
- 聚合会忽略请求范围之外的记录,且只有在严格多数时才会选出主导时间桶。
- 跨表写入——产品、成分、解析记录、计划、摄入、结果、库存、通知偏好——在事务中执行。
- 正式的洞察标识在窗口升级后依然保持不变,因此一条从 14 天窗口成长为 28 天窗口的结论不会被当作新结论上报。
07验证与发布工程
验证以证据为准:每个发布步骤都会连同命令、日志与产出的制品一起记录,任何事项都不会因为「本来打算做」而被标记为完成。
发布路径
- GitHub Actions 构建并导出已签名的发布归档;本地验证用 Swift Testing 运行包测试。
- App 与小组件各自带有独立的 App Store 分发描述文件,并共享同一个 App Group 授权,上传前会对两者的签名做递归校验。
- 构建产物会交付到 App Store Connect,通过 TestFlight 实测,并依据 App 审核反馈迭代。
- 专门的校验器覆盖 App 图标、隐私政策页面、发布契约与截图完整性,让合规制品在出问题时大声失败,而不是悄悄漂移。
关于审核迭代。App 审核反馈促成了把真实修复做回正式发布构建——包括用户手动填写无法识别的成分时的校验行为。处理这个闭环本身就是工作的一部分,而不是对工作的打断。
08它刻意不做的事
一款健康产品的早期版本总会积累起各种诱人的功能。以下这些是刻意排除在范围之外的,App 在构建与发布时都不包含它们:
- 不做诊断、剂量建议、相互作用警告或治疗指导。
- 不做因果断言——关联永远不会被当作证明来呈现。
- 不做化验报告或血液指标分析。
- 没有账号、同步、云存储或照护者共享。
- 不向 Apple Health 写入;读取是只读的,而且是可选的。
- 没有广告、跟踪、分析或支付 SDK。
- 没有 iPad、Mac、Apple Watch、Android 或 Web 客户端。
- 不声称获得监管批准或具备医疗器械资质。
早早划下这条线,让领域保持诚实:每一个留下来的功能,都必须相对上述分析层面的保证证明自己的必要性。
09状态与链接
Elemental 1.0 已完成、已签名并已交付,截至 2026 年 9 月已提交 App Store 审核。App 仅支持 iPhone,需要 iOS 26 或更高版本,且不包含任何由开发者运营的网络端点。