最近 7 天新增下载为零,但我还是把 1.9 发了
这是我独立开发的存钱 App「聚沙攒钱」的真实数据。说出来有点难受,但我觉得版本该发还是得发——不能因为没人下载就停止打磨产品。1.9 的重点是重构成就徽章系统、新增 365 天挑战,还有一些一直想解决的性能问题。
这篇文章聊聊这次更新里几个技术决策背后的思考,特别是成就系统的设计和双模式架构的取舍。
成就系统重构:从暴力遍历到摘要驱动
之前的做法很糙:用户每次存款后,遍历所有徽章条件,逐一查数据库判定是否解锁。目标少的时候没感觉,但当用户有十几个目标、几百条存款记录时,每次存钱后会卡大概 200ms——在动画播放的关键帧里,这个延迟肉眼可见。
重构思路是把高频查询的统计数据预计算成一个 StatsSummary 摘要,存款时只更新摘要的对应字段,徽章判定变成纯内存操作:
struct BadgeDefinition: Identifiable {
let id: String
let name: String
let category: String
let condition: (StatsSummary) -> Bool
}
// 每个徽章就是一个闭包
BadgeDefinition(id: "streak_7", name: "Week Streak", category: "streak") {
$0.currentStreak >= 7
},
BadgeDefinition(id: "night_owl", name: "Night Owl", category: "special") {
$0.nightDeposits >= 10
},
BadgeDefinition(id: "ritual_master", name: "Ritual Master", category: "special") {
$0.ritualCount >= 5
},
BadgeDefinition(id: "collector", name: "Collector", category: "special") {
$0.activeGoals >= 5
}
新增徽章只需要一行定义,不改业务代码。目前一共 16 个徽章,停在这个数字是刻意的——我试过加更多(比如「连续 3 天存款金额递增」「周末存款达 10 次」),但测试下来发现条件太多用户反而没有目标感,看着一堆灰色锁头会焦虑。16 个大概是一屏半能展示完的量,用户划两下就能看全。
几个我觉得比较有意思的徽章设计:
- Night Owl / Early Bird:凌晨 0-5 点存款 10 次解锁夜猫子,早上 5-8 点 10 次解锁早起鸟。这两个是行为洞察——固定时间段存钱的人留存明显更好,用徽章做正向引导
- Ritual Master:完成 5 次「砸罐」仪式(目标达成后的庆祝动画)才解锁,意味着用户至少完成过 5 个储蓄目标
- Starter Week:新用户引导专用,要求同时满足「建了目标 + 存过款 + 连续 2 天」,门槛很低但把三个关键行为串起来了
双模式架构:丑代码和没选策略模式的原因
App 有两种核心模式:愿望模式(wish,有目标金额)和自由模式(free,纯粹存钱罐没上限)。
自由模式是上线后用户反馈加的——有人说「我就想随手存零钱,不想设目标」。加了之后 targetAmount 可能为 0,进度计算、完成状态、UI 展示全部要分支处理,代码里到处是 if goal.mode == .free。
我确实考虑过用协议抽象,定义 SavingsGoalProtocol,让 WishGoal 和 FreeGoal 各自实现进度计算。但实际情况是:这两种模式共享 95% 的逻辑(存款、打卡、统计、备份),真正不同的只有「进度百分比怎么算」和「是否显示完成状态」这两个点。为了两个 if 引入一套协议体系,我觉得是过度设计。
策略模式也想过——把进度计算抽成 Strategy 对象注入。但 SwiftData 的模型层不太方便持有策略对象,序列化的时候还要额外处理。最后的决定是忍受 if 判断,把所有分支集中在一个 extension 里,至少改的时候知道去哪里找。
丑吗?丑。但作为一个人的项目,能跑、好改、不出 bug 比优雅重要。
挑战模式:52 周存钱法的妥协
挑战模板四种:30 天、52 周、100 天、365 天。
ChallengeTemplate(
id: .week52,
titleKey: "Challenge 52 Weeks",
icon: "📈",
totalDays: 364,
suggestedTargetAmount: 1378 // 1+2+3+...+52
)
52 周的 suggestedTargetAmount 是 1378,对应经典的递增存法。我一开始想做成强制每周递增——第 1 周必须存 1 块,第 2 周必须 2 块,不满足就算断签。后来自己测了一轮,到第 35 周(每周 35 块)就开始觉得烦了,对学生用户来说更是压力。
最终方案是只记录总进度,UI 上提示「本周建议存 XX 元」但不强制。保留了仪式感,去掉了压迫感。
365 天挑战是 1.9 新加的,配合本地 JSON 备份一起上线。一年跨度太长,用户换手机的概率不低,备份文件带了 schema version 方便以后做格式迁移,限制最大 8MB(正常用几年也到不了)。
SpriteKit 硬币动画的性能坑
这是用户感知最强的功能:存款后硬币从顶部落下,碰到罐壁弹跳堆叠。用 SpriteKit 物理引擎,每个硬币一个 SKSpriteNode + physicsBody。
踩的坑:用户一次存 1000 块,按 10 块一个硬币就是 100 个节点同时参与物理模拟,iPhone SE 2 上帧率直接掉到 30fps 以下。
解决方案:同时渲染的硬币上限 30 个,超出的用加速动画直接落入底部合并,视觉上是「哗啦一下」的感觉反而更爽快。CPU 占用从峰值 45% 降到 18% 左右。
下载量为零的反思
技术层面我对 1.9 还算满意,但产品推广确实是短板。分析了几个原因:
- ASO 关键词选的「存钱」「攒钱」,但用户实际搜索习惯偏向「记账」,搜索量差了一个数量级
- 硬币动画这个卖点在静态截图里完全体现不出来,需要录视频但我一直没做
- 25-35 岁有储蓄意愿的目标群体不太会在 App Store 主动搜「存钱 App」——他们更可能在社交平台看到别人的打卡截图后被种草
下一步打算做的:挑战模式完成阶段打卡后生成分享卡片,让用户帮我做传播。这可能是目前成本最低的获客手段。
如果你也在做游戏化设计
我在徽章条件的设计上花了不少时间——什么样的条件既有挑战性又不让人焦虑,门槛设多高用户才会觉得「差一点就够到了」而不是「算了太难了」。这些没有标准答案,我也还在摸索。
如果你的产品里也有成就系统或者游戏化激励的设计,评论区聊聊?特别想知道大家怎么定阈值的——是靠数据分布还是靠手感。