起因:记账App太多,存钱App太少
去年年底我想找一个纯粹帮我"攒钱"的工具。注意,不是记账,是攒钱——我想设一个目标,比如攒2万块去日本玩,然后每次往里扔点钱,看着进度条一点点涨上去。
找了一圈,发现要么是记账工具顺带有个储蓄目标功能(藏得很深),要么就是理财平台包装的导流入口。我就想要一个简单的东西:设目标、存钱、看进度、有点成就感。
没找到,那就自己写一个。这就是「聚沙攒钱」的由来。
两个核心模式想了挺久
一开始我只做了"愿望模式"(对应代码里的wishmode)——设一个金额目标,比如买AirPods Max要4399,然后每次手动记一笔存入。很直觉,零门槛。
但做着做着我发现一个问题:很多人(包括我自己)存钱坚持不下去,不是因为没钱,是因为没有节奏感。今天存500明天忘了后天又存200,乱七八糟的。
所以我加了"聚沙模式"(对应代码里的freemode,自由定投的意思)。你设定每周存300,App帮你算出多少周能到目标,还有个简单的复利计算器。核心不是算得多精确,是给你一个固定节奏——每周到了那天,该存钱了。策略字段支持weekly、biweekly、monthly三种频率。
这两个模式我纠结了大概两周,试过合并成一个、试过加更多模式,最后发现就这两个最清晰。短期愿望用 wish,长期积累用 free。
硬币掉落动画:花时间最多的功能
说实话,整个App里花时间最多的不是数据层,是那个存钱时硬币哗哗掉下来的动画。
我用 SpriteKit 做了物理引擎驱动的硬币掉落效果。每次点"存入",屏幕上掉下来一堆金币,带碰撞、带弹跳,落到罐子里。核心场景配置大概长这样:
// 硬币掉落场景的关键配置funcspawnCoins(foramount:Double){letcount=min(Int(amount/10)+3,60)// 存10块掉3枚,500块掉一大堆,上限60physicsWorld.gravity=CGVector(dx:0,dy:-4.8)foriin0..<count{letcoin=SKSpriteNode(imageNamed:"coin_gold")coin.size=CGSize(width:28,height:28)coin.position=CGPoint(x:CGFloat.random(in:frame.midX-80...frame.midX+80),y:frame.maxY+CGFloat(i*12)// 错开高度,不要一坨砸下来)coin.physicsBody=SKPhysicsBody(circleOfRadius:14)coin.physicsBody?.restitution=0.35// 弹性系数,调了很多次coin.physicsBody?.friction=0.3coin.physicsBody?.linearDamping=0.1addChild(coin)}}``` `restitution`(弹性系数)这个参数调了很多次。0.6的时候硬币会弹得到处飞,像弹力球一样;0.1又完全没弹跳感,像泥巴糊上去的。最后定在0.35,看起来有真实感又不会飞出屏幕。 听起来是个花哨的小功能对吧?但我发现这东西对坚持存钱的影响比我想象的大。存钱本身是个反人性的动作——你在限制当下的消费。如果存完了只是一个数字从1800变成2100,那感觉跟在Excel里改个数没区别。但硬币哗啦啦掉下来,配上触觉反馈(UIImpactFeedbackGenerator),那一瞬间是有"获得感"的。 我女朋友用了之后说"存钱的时候听那个声音特别爽",后来给几个同事装了,反馈也差不多——功能层面大家觉得还行,但提到最多的就是这个硬币动画。游戏设计里叫"juice",我只是把它搬到了一个理财工具里。 ## 成就徽章系统:用游戏化对抗懒惰 另一个我觉得做对了的设计是徽章系统。每个徽章的解锁条件是一个闭包,接收用户的统计摘要,返回布尔值: ```swiftBadgeDefinition(id:"streak_7",name:"Week Streak",description:"Deposit 7 days in a row",category:"streak"){$0.currentStreak>=7},BadgeDefinition(id:"night_owl",name:"Night Owl",description:"Deposit 10 times at night",category:"special"){$0.nightDeposits>=10},BadgeDefinition(id:"early_bird",name:"Early Bird",description:"Deposit 10 times early morning",category:"special"){$0.earlyDeposits>=10}``` 目前一共13枚徽章,覆盖存款里程碑、连续打卡、目标数量、特殊时段这几类。13枚不算多,但人真的会为了解锁某个徽章刻意去凑条件。我女朋友专门设了个闹钟早上6点存10块钱,就为了攒"Early Bird"——连续攒了两周才攒够10次。 这个闭包的设计扩展性不错,后来我想加什么新徽章,写一个 `BadgeDefinition` 丢进数组就行,不用动任何其他代码。 ## 每日语录:324句组合拼接App里有个每日激励语录功能,一开始我觉得太鸡汤想砍掉。后来决定自己写,用了一个偷懒但效果不错的办法——主语谓语拼接:18个主语("一杯奶茶的钱"、"睡前的那一次点击"、"坚持7天的连续记录"……)配18个谓语("会慢慢变成踏实的安全感"、"让计划不会停在第一天"……),按日期做哈希取模,18×18=324种组合,够一年不重复。 这种方式写出来的语录比AI生成的有烟火气得多。而且偶尔会出现意想不到的搭配,比如"把红包攒起来,是最便宜的安心保险"——这句我自己都觉得挺好。 ## 数据备份踩的坑:JSON体积爆炸 这个App后来我也做了HarmonyOSNEXT版本,用ArkTS重写。在做数据备份功能时踩了一个印象深刻的坑。 iOS版用CoreData,备份走系统的序列化机制,体积控制不太用操心。但鸿蒙版用的是轻量级本地存储,导出就是纯JSON。有个测试用例里灌了1200个目标,每个关联20笔交易,导出的JSON直接撑到几十MB,在部分设备上解析卡死。 后来我加了8MB的硬上限(`MAX_BACKUP_TEXT_LENGTH=8_000_000`),超过的时候按目标维度分片——每个分片包含一批目标及其关联的交易记录,保证单个目标的数据完整性不被拆断。每个分片有自己的 `schemaVersion` 和 `partIndex`,恢复时按 `partIndex` 顺序合并,合并完做一次目标ID去重校验,防止重复导入。 说实话这个方案不算优雅,但对于一个本地工具来说够用了。真有用户攒到1200个目标……那他可能需要的不是存钱App而是心理咨询。 ## 目前的状态 聚沙攒钱 iOS 版目前1.8版本,鸿蒙版也在华为应用市场上了。下载量说实话不大,还在慢慢推。没做任何付费功能的硬推,Pro版本只是解锁更多徽章和主题色,基础功能全免费。 回头看,做得最对的一件事是坚持把硬币动画做出来。很多时候我们做工具类App会觉得"功能全了就行,动画是锦上添花"。但对于一个帮人养成习惯的产品,那个"花"可能比"锦"重要。 ## 技术栈-iOS版:SwiftUI+SpriteKit(物理动画)+CoreData+AppIntents--HarmonyOS版:ArkUI+ArkTS,Hvigor构建--纯本地存储,用户数据不出设备 对了,鸿蒙版我一直没找到好的方案来做类似SpriteKit那种物理动画效果,ArkUI的动画能力和原生物理引擎之间差距还是挺大的。如果有做鸿蒙开发的朋友在这块有经验,评论区聊聊? ## 相关链接-AppStore搜索「聚沙攒钱」(ID:6758853486)--华为应用市场搜索「聚沙攒钱」