用 SpriteKit 做了个存钱罐 App,30 枚硬币同时掉帧率直接崩了

起因

去年底想做一个纯本地的存钱记录工具。看了一圈市面上的产品,存钱这个动作基本就是输入数字、点确认,存完没感觉。我想要的是:每次存钱时屏幕上有硬币哗哗掉进罐子里,带物理碰撞和堆叠效果,给这个动作加一点正反馈。

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%,但没有数据完全在盲猜。有经验的欢迎评论区聊聊。

相关推荐
黑化旺仔6 小时前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan8 小时前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo8 小时前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记14 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程14 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne20 小时前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记2 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩2 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年2 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008892 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp