做了个呼吸 App,动画同步踩了三个坑——SwiftUI 方案最终跑稳了

开会前焦虑这件事困扰我挺久的。试过市面上几款呼吸 App,广告一弹、会员弹窗一出,焦虑反而更重了。后来想,需求这么简单,不如自己写一个——打开,跟着做两分钟,关掉。没有注册,没有打扰。这就是「呼吸视界」的起点。

App 做了什么

功能不复杂,就三块:

引导练习:内置几种经典呼吸法,4-7-8(吸气 4 秒、屏息 7 秒、呼气 8 秒,睡前放松用)、盒式呼吸(4-4-4-4,据说军队用来对抗急性压力)。每种呼吸法有视觉引导动画,跟着节奏走,不用自己数秒。

课程计划:单次练习有用,但坚持才有效果。我做了一个 ProgramProgressRecord 记录用户在每个课程里练到哪节,下次打开自动续上,不用重新找进度。

训练历史:全部本地存储,不上云,不需要注册。呼吸练习属于私人健康数据,我自己也不想交出去,这个决策是有意为之的。

动画同步:前两个方案都扔掉了

呼吸引导动画的核心难点是「动画帧和呼吸阶段要精确同步」。用户吸气 4 秒,圆形扩张就得恰好 4 秒跑完。差一点,用户能感知到,反而更烦躁。

第一个方案:CADisplayLink 手动算 phase 进度。精度理论上够,但 phase 切换时有跳帧——相邻两个 phase 的起始帧之间有约 1 帧的空白,圆形切换瞬间会轻微抖动。根本原因是 CADisplayLink 回调时机和 phase 边界不对齐,差一帧的距离,找了很久也没彻底解决,放弃。

第二个方案:Timer + UIView.animate。Timer 在 iOS 后台节能机制下会漂移,低电量模式实测 4 秒的 phase 可能跑成 4.3 秒,圆形已经跑完但下一阶段还没触发,用户就傻等着。这个精度对普通 UI 无所谓,但对呼吸引导体验直接崩。

最终方案:SwiftUI withAnimation 配合 DispatchQueue.asyncAfter

关键点在于:withAnimation(.linear(duration:)) 的 duration 和 asyncAfter 的 deadline 来自同一个 phase.duration,物理上不可能错开。跳帧和漂移都消失了。

func startPhase(_ phase: BreathPhase) {
    currentPhase = phase
    progress = 0.0
    withAnimation(.linear(duration: phase.duration)) {
        progress = 1.0
    }
    // asyncAfter 在 App 切入后台后可能被挂起
    // 后台容错还没做完,是下一个要填的坑
    DispatchQueue.main.asyncAfter(deadline: .now() + phase.duration) { [weak self] in
        guard let self else { return }
        let next = self.currentPhase.next(for: self.pattern)
        self.startPhase(next)
    }
}

每个 phase 结束时自动推进下一个,动画和等待时间绑死在同一个值上。主流设备跑得很稳,极端低电量和频繁前后台切换是下一个坑。

存储选型:为什么没用 CoreData

ProgramProgressRecord 字段不复杂:课程 ID、当前节索引、最近练习时间、总完成次数。用 Codable 序列化后写进 UserDefaults

选择理由很具体:用 JSONEncoder 实测了一下,单条 record 序列化约 180 字节,App 内课程总数 20 个左右,全部存下来不到 4KB。CoreData 在这个数据量上是杀鸡用牛刀,还多了 schema 迁移的风险。SwiftData 倒是可以考虑,但引入新依赖对这个体量的 App 不值得。简单的东西用简单的方案。

克制比丰富更难

做这类正念工具,UI 的诱惑是往简约风靠,磨砂玻璃、渐变光晕、粒子效果……我自己也差点走这条路。

后来把装饰性动画删掉了大半。用户打开 App 是来「平静」的,不是来「被 App 的设计感动」的。动效多了本身就是噪音。最终锁定深色背景加单一强调色,字体只用系统字体,间距全部用 enum Spacing token 约束,不在代码里写裸数字。视觉上「没那么惊艳」,但用起来不累。

现在的状态

App Store 2024 年底上线,目前版本 1.8,评分 5 分。下载量不多,早期靠朋友圈传播。我自己用得最多的场景是开会前做一轮 4-7-8,和睡前用来替代刷手机。有用户留言说「可以跟随练习呼吸,保持稳定的心情」,看到这条挺高兴的,说明东西真的有人在用。

接下来想做的:练习结束后加简短的感受记录(纯文本,不打标签)、支持 Apple Watch 震动引导,以及后台挂起时的动画同步容错——最后这个坑必须填。


withAnimation + asyncAfter 这套在主流设备上跑稳了,低电量极端场景欢迎来踩。如果你做过类似需要精确节奏同步的引导动画(呼吸类、冥想类都行),评论区聊聊——有没有比 asyncAfter 更优雅处理后台挂起的路子,我还没想到好方案。

App Store 搜索「呼吸视界」可以找到,免费无广告。

posted @ 2026-04-26 19:27  samexxx  阅读(9)  评论(0)    收藏  举报