起因
去年底想做一个纯本地的存钱记录工具。看了一圈市面上的产品,存钱这个动作基本就是输入数字、点确认,存完没感觉。我想要的是:每次存钱时屏幕上有硬币哗哗掉进罐子里,带物理碰撞和堆叠效果,给这个动作加一点正反馈。
App 叫「聚沙攒钱」,iOS 版目前到 1.9。这篇主要聊硬币动画的实现、帧率问题怎么解决的,以及储蓄系统的架构选择。
硬币掉落:为什么选 SpriteKit
一开始考虑过 Core Animation 手写弹跳曲线,试了两天放弃了------硬币之间需要碰撞堆叠效果,多个刚体互相挤压的物理模拟手写贝塞尔根本搞不定。
最后引入了 SpriteKit。说实话在工具类 App 里用游戏引擎有点重,但 SKPhysicsBody 天然支持重力和碰撞检测,省了我自己造轮子。
核心逻辑:
swift
func dropCoin(at position: CGPoint) {
let coin = SKSpriteNode(imageNamed: "coin_gold")
coin.size = CGSize(width: 28, height: 28)
coin.position = CGPoint(x: position.x, y: frame.maxY + 20)
coin.physicsBody = SKPhysicsBody(circleOfRadius: 14)
coin.physicsBody?.restitution = 0.3
coin.physicsBody?.friction = 0.4
coin.physicsBody?.linearDamping = 0.1
addChild(coin)
}
restitution 调到 0.3 是反复试出来的------0.5 以上硬币落地像橡皮球一样蹦,看着很假;0.1 以下又完全没弹性,像石头砸地上。
30 枚硬币同屏的帧率问题
存一笔大金额时,我会根据金额映射生成多枚硬币(上限 30 枚)。第一版是一次性全部 addChild,结果在 iPhone SE 上帧率直接掉到 40fps 以下,能肉眼看到卡顿。
原因也简单:30 个刚体同一帧开始做碰撞检测,物理引擎计算量瞬间拉满。
解决方案是加一个延迟队列,每枚硬币间隔 0.08 秒生成。视觉上反而更像「哗哗倒进去」的感觉,比一坨同时出现要自然。iPhone SE 上稳定 60fps,问题就没了。
这算是整个项目里最典型的一个教训:物理引擎不怕节点多,怕的是同一帧突然涌入大量碰撞计算。
双模式架构:wish 和 free
储蓄逻辑上我分了两种模式。代码里对应 GoalMode 枚举的两个值:
.wish(愿望模式):设定目标金额,比如攒 8000 买 iPad,按 weekly/biweekly/monthly 的策略生成定期计划,App 算出需要多少期、每期多少钱,到期提醒你执行.free(自由模式):不设目标不设周期,纯粹当个零钱罐,随手存随手记
swift
struct Goal {
let mode: GoalMode // .wish = 目标驱动 | .free = 自由存入
let strategy: Strategy // .weekly / .biweekly / .monthly(仅 wish 模式有效)
let targetAmount: Int64 // wish 模式的目标金额,free 模式为 0
let currentAmount: Int64
let totalPeriods: Int // 计划总期数,free 模式为 0
let nextDueDate: Date? // 下次应存日期,free 模式为 nil
}
.free 模式是后来补的。有用户反馈说「我就想随手记一笔,不想被目标和倒计时绑着」。想想也对,并不是所有人都适合目标驱动型的存钱方式。
成就徽章的条件判定
为了维持打卡动力,做了 16 枚成就徽章。每个徽章的解锁条件定义在 BadgeDefinition 里,绑定一个接收 StatsSummary 的闭包:
swift
BadgeDefinition(
id: "night_owl",
name: "Night Owl",
description: "Deposit 10 times at night",
category: "special"
) { stats in
stats.nightDeposits >= 10
}
每次打开成就页面,遍历所有 BadgeDefinition,把当前用户的统计数据传进去跑一遍条件。不需要额外的状态机或者解锁记录表------徽章数量少(16 个),统计数据也是预聚合好的,跑一遍几乎无开销。
设计徽章的难点不在代码,在于门槛怎么定。连续 7 天大概 30% 用户能达到,365 天挑战就是给极少数人的荣誉勋章。门槛太低没成就感,太高让人放弃。
提醒通知
默认每天 20:15 推一条本地通知提醒存钱。这个时间点是我自己拍的------晚饭后睡前,大部分人比较放松,愿意花 10 秒点一下。
用户可以改时间,但改的人很少。
这里有个问题困扰我:我没接任何统计 SDK,完全不知道通知的实际打开率是多少。本地通知不像推送服务有送达回执,UNUserNotificationCenter 的 delegate 方法只在用户点击通知时触发,如果用户看到了但没点,我这边就是个黑盒。
下个版本打算至少在 userNotificationCenter(_:didReceive:) 里记一笔本地打点,算个点击率出来。
隐私遮罩
有个小功能:App 进后台时自动遮住金额内容,防止多任务切换被人瞥到。需求来源就是我自己------有次在公司切任务时同事正好看到我的存钱目标,不是什么秘密但确实有点尴尬。
配置项叫 privacyMaskOnBackground,监听 scenePhase 变化就行,实现简单但体验上确实需要。
目前的状况
说实话数据很一般,近一周下载量基本是零。没做过付费推广,ASO 也处于裸奔状态。
技术上我对 SpriteKit 动画的效果挺满意,徽章系统的扩展性也够用。但"做得出来"和"有人用"完全是两码事。
回到提醒通知这个点------如果有同行也在做工具类 App 的留存,你们的每日提醒通知点击率大概在什么水平?我猜应该不超过 10%,但没有数据完全在盲猜。有经验的欢迎评论区聊聊。