做了一个健康记录 App,聊聊 SwiftData + 拨轮交互的实现思路

起因

去年我爸确诊高血压,医生让每天记录血压。试了几个 App,要么界面花哨操作繁琐,要么强制订阅一年好几百。我爸一个六十多岁的人,打开 App 看到数字键盘就烦躁,经常忘记记。

所以我自己写了一个。叫「健康手账」,iOS 原生,SwiftData 存储,核心卖点就一个------录入快,3 秒搞定一次血压记录。

拨轮交互这事儿

说白了就是把数字键盘干掉了。血压值的范围其实很固定,收缩压大概 80-200,舒张压 40-130。用拨轮比用键盘快得多,手指一拨就到位,还带物理阻尼感的触觉反馈。

我观察我爸用手机的习惯:他对「滑动」这个动作很熟练(刷短视频嘛),但对精确点击小按钮很抗拒。拨轮正好契合这个习惯。

实际测下来,从打开 App 到完成一次血压录入,大概 10 秒。如果加上 Siri Shortcuts,连打开都省了:

swift 复制代码
struct LogHealthRecordIntent: AppIntent {
    static let title: LocalizedStringResource = "记录健康数据"
    static let openAppWhenRun: Bool = true

    @MainActor
    func perform() async throws -> some IntentResult {
        try await Task.sleep(for: .milliseconds(200))
        NotificationCenter.default.post(name: .healthLogShowInputSheet, object: nil)
        return .result()
    }
}

对着手机说一句"用健康手账记录血压",直接弹出录入界面。我爸现在用得还挺顺。

数据模型的取舍

一开始我把血压、体重、血糖拆成不同的 Model。后来发现这样查询和展示都麻烦,改成了单一 HealthRecord,用可选字段区分类型:

swift 复制代码
@Model
final class HealthRecord {
    var id: UUID
    var timestamp: Date
    var systolic:  Int?
    var diastolic: Int?
    var pulse:     Int?
    var weight:    Double?
    var note:      String?
    var tagIDs:    [String]
    var profileID: String = "default"
}

systolic 有值就是血压记录,weight 有值就是体重记录。简单粗暴,但查询的时候一个 predicate 就能过滤,趋势图渲染也方便。

有人可能觉得这样不够"规范",但对一个独立 App 来说,少建一张表就少一堆迁移问题。SwiftData 的 schema migration 说实话踩过几个坑,能简单就简单。

状态印章------把干预行为和数据关联

这个功能我觉得挺有意思。用户记录血压的时候可以顺手打一个"印章",比如今天吃了降压药、做了运动、喝了酒。

底层是一个 StatusTag 模型,默认预设了降压药、运动、好睡眠这些,用户也能自己加。每条 HealthRecord 通过 tagIDs 数组关联。

这样在看趋势图的时候,能直观看到"吃药那几天血压稳定,停药这两天又上去了"。我带我爸看诊的时候,医生看到这个关联还挺高兴,说比口头描述"好像最近有吃药"靠谱多了。

PDF 就医报告

这个功能是被逼出来的。每次去医院,医生问"最近血压怎么样",我爸掏出手机给医生看,医生根本没时间翻你的 App。

所以做了一键导出 PDF 报告:选个时间范围,生成一份带折线图和数据表格的单页 PDF,打印出来递给医生就行。格式参考了几份正规的血压监测报告模板。

说实话这个功能开发成本不高,用 UIGraphicsPDFRenderer 画就行。但用户价值挺大,同类免费 App 里基本没人做这个。

多人档案

profileID 这个字段就是为了支持家庭场景。我妈也有高血压,我不想装两个 App 或者切账号。现在一个 App 里建两个档案,数据完全隔离,趋势图和报告都是独立的。

实现上就是所有查询都带 profileID 过滤,Widget 和提醒也跟着当前选中的档案走。

商业模式

买断制,不搞订阅。慢病管理这种东西用户可能要用好几年,订阅制对他们来说心理负担太大。Pro 版解锁多人档案和 PDF 导出,基础的单人记录和趋势图免费用。

目前还在冷启动阶段,下载量很小。但我自己每天都在用,我爸妈也在用,这个 App 对我来说已经有价值了。

一些技术细节的坑

  • SwiftData 的 cloudKitDatabase: .automatic 在用户 iCloud 没登录时会静默失败,我加了个手动开关让用户自己决定要不要同步
  • WidgetKit 读不了 SwiftData 的数据库,我单独写了个 WidgetDataStore 用 App Group 的 UserDefaults 传数据
  • @Observable 配合 SwiftData 的 @Model 混用时偶尔有刷新时序问题,最后把设置类和数据类彻底分开了

回头看

这个项目从立项到上架大概花了六周的业余时间。对我来说最大的收获是:给真实的人(我爸)解决真实的问题,比做一个"有市场前景"的东西有动力得多。每次看到我爸晚上测完血压顺手拨两下就记好了,比任何下载数据都让我开心。

如果你也在做健康类或者工具类的独立 App,欢迎评论区聊聊你们的录入交互方案,我对这块挺感兴趣的。

相关推荐
终端安全笔记3 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程3 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne9 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记1 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩1 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年1 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008891 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp
HouWan2 天前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios
用户38034165882972 天前
Broadcast Extension 里的 Live Activity:一次跨进程妥协
ios·音视频开发
匠测AI说2 天前
XCUITest 自动化全解:同进程白盒、XCTest 架构、自动同步机制与 iOS 端选型指南
测试工具·ios·自动化