做了一个健康记录 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 小时前
iOS视频自动全屏问题解决方案
ios·音视频
Bug 挖掘机5 小时前
从0到1做出可复用的 iOS 自动化测试 Skill,附真机演示效果
自动化测试·测试开发·ios
掘根5 小时前
【微服务即时通讯】客户端通信连接
ios·iphone
00后程序员张6 小时前
完整指南 iOS App上架到App Store的步骤详解
macos·ios·小程序·uni-app·objective-c·cocoa·iphone
鹤卿1236 小时前
Block基础
开发语言·ios·objective-c
开开心心loky7 小时前
[OC 底层] (二)类与对象底层原理
macos·ios·objective-c·cocoa
ZZH_AI项目交付21 小时前
扫脸功能交给 SDK 后,主工程里的旧代码怎么删除
ios·app·apple
ZZH_AI项目交付1 天前
扫脸功能做成 SDK,为什么我没有把结果页和历史记录一起搬进去
ios·app
茶底世界之下1 天前
诡异!String 参数在闭包里变成了 <uninitialized>,我排查了整整两天
ios·xcode·swift