起因
上个月翻信用卡账单,发现一笔 Peacock Premium 的扣费——我压根不记得什么时候订的这玩意儿。查了一下,试用期三个月前就结束了,白白扣了三个月。
这事儿让我挺难受的。我自己就是开发者,手机里一堆工具类 App 的订阅,加上各种流媒体,每月到底花了多少钱在订阅上,说实话我算不清楚。
市面上订阅管理类的 App 不少,但我试了几个,体验都偏「记账」——你把东西录进去,它给你个列表,然后就没了。取消订阅这个动作被藏在一个不起眼的角落,整体缺乏动力。
所以我做了「订阅斩」,核心想法就一个:把停用订阅变成一件有成就感的事,而不是一个被动的管理行为。
技术选型:SwiftData + 游戏化状态机
整个数据层用了 SwiftData,模型设计上我做了一个关键决定:订阅状态不是简单的 active/inactive 布尔值,而是一个状态枚举,配合 killedDate 字段来记录「斩杀」时间。
@Model
final class Subscription {
var id: UUID
var name: String
var price: Double
var cycle: Int // 0=月付, 1=季付, 2=年付
var isTrial: Bool
var nextBillingDate: Date
var status: Int // active / killed
var killedDate: Date? // 斩杀时间戳
var isShared: Bool
var membersCount: Int
var icon: String
var unsubscribeURL: String
var billingCycle: BillingCycle {
BillingCycle(rawValue: cycle) ?? .monthly
}
}
为什么要单独存 killedDate?因为「已斩」列表是按斩杀时间排序的,用户能看到自己的「战绩」——这个月斩了 3 个,省了多少钱。这种反馈比单纯的删除操作强很多。
月度支出计算:统一到月维度
订阅有月付、季付、年付三种周期,要算「每月到底花多少」就需要统一换算。我用了一个 multiplierToMonthly 属性:
enum BillingCycle: Int, CaseIterable {
case monthly = 0
case quarterly = 1
case yearly = 2
var multiplierToMonthly: Double {
switch self {
case .monthly: 1
case .quarterly: 1.0 / 3.0
case .yearly: 1.0 / 12.0
}
}
}
这样算月支出就是 price * billingCycle.multiplierToMonthly,一行搞定。看起来简单,但我一开始犯了个错——直接用除法存浮点结果到数据库里,后来发现精度丢失导致总额对不上,改成存原始价格 + 周期枚举,展示时再计算,才对了。
风险检测怎么做的
这个功能我觉得是差异点。逻辑其实不复杂:
- 高危:试用期订阅,距离下次扣费不到 3 天
- 中危:订阅超过 6 个月没有主动操作过(没改过备注、没点进详情)
- 低危:正常续费中,最近有交互
说白了就是根据 isTrial、nextBillingDate 和最后交互时间做简单分级。但这个分级展示出来的效果比我预想的好——用户会被「高危」两个字刺激到,去检查那些被遗忘的订阅。
预设订阅目录
我内置了一个 SubscriptionCatalog,预设了 Netflix、YouTube Premium、Disney+ 这些常见服务的默认信息(图标、默认周期、参考价格、是否推荐拼车等)。用户添加订阅时输入名字,会做模糊匹配自动填充。
这个做法带来一个好处:降低了录入成本。我自己测试录入 12 个订阅,用目录匹配只需要打前几个字母然后确认,大概 2 分钟搞定。如果每个都手动填价格和周期,至少要 5 分钟。
多币种适配
做了一个根据设备 locale 推断默认货币符号的逻辑,覆盖了 CNY、USD、EUR、GBP、JPY、KRW、TWD、HKD 等主要币种。这个看着小,但对非大陆用户来说体验差距很大——没人想在一个人民币符号的界面里输美元金额。
一些踩坑和取舍
iCloud 同步:SwiftData 配合 CloudKit 的坑比我想象的多。最头疼的是 schema migration——新增字段如果没给默认值,旧数据同步过来直接崩。所以我把后加的字段全部做成了 Optional(比如 remindersEnabledStorage: Bool?),虽然用起来要多写 unwrap 逻辑,但比处理 migration crash 省心。
提醒功能:做了到期前 7 天、3 天、1 天三档提醒,用户可以自选组合。本来想做更细粒度的(比如精确到几点推送),后来想了想这不是个日程工具,没必要。砍了。
OCR 识别账单:这个功能做了但限制了免费次数。说实话识别率一般,银行账单的格式太多了,我目前只能覆盖几种常见模板。后续看看要不要接入更好的模型。
目前的状况
App 刚上线不久(1.2 版本),还在冷启动阶段,没什么自然流量。产品我自己每天在用,体验上我觉得够顺了。
做这个 App 最大的收获是:游戏化不一定需要复杂的积分系统或者排行榜,有时候就是把一个动作的命名和反馈改一下。"删除订阅"和"斩断订阅"做的是同一件事,但用户感受完全不同。前者是打扫卫生,后者是打怪升级。
对了,如果你也在做独立 App 的订阅管理或者个人财务类方向,欢迎交流。关于 SwiftData 在这类场景下的实际体验,我踩的坑还挺多,评论区可以聊。