我做了一个「斩订阅」的 iOS App,用 SwiftData 管理那些悄悄扣钱的服务

6 阅读5分钟

上个月查信用卡账单,发现一个扣了我 18 块的订阅——某个 AI 写作工具,试用期过了自动续费,我完全忘了它的存在。这事儿让我有点难受,不是因为钱,是因为这种「被偷走」的感觉。

查了一圈发现,我当时在跑的订阅服务加起来每月快 300 块,其中至少三个我已经半年没打开过了。

于是我做了「订阅斩」(SubKiller),一个帮你追踪、审查、干掉不需要订阅的 iOS App。

产品核心逻辑:不只是记账

市面上的订阅管理工具大多是「记录型」的——你告诉它你有什么,它帮你列出来。但我觉得这不够,因为问题根本不是「不知道自己有哪些订阅」,而是懒得去审查它们

所以我在 SubKiller 里加了一个风险检测机制:App 会根据订阅状态(比如距上次使用时间、是否在试用期、账单周期是否临近)给每个订阅打风险等级,分高危和中危。高危订阅会主动推提醒,不用你自己去翻。

另一个设计是「斩」的交互。停用一个订阅不叫「删除」,叫「斩断」——有个动效,斩断后的订阅会进入「已斩」Tab,可以随时恢复。这个设计带来一点仪式感,把「停掉一个没用的订阅」变成一个有成就感的主动动作,而不是默默删个记录。

技术实现:SwiftData + BillingCycle 折算

数据层用的 SwiftData,Subscription model 存了名称、价格、账单周期、下次扣费日期、状态等字段。

账单周期是我花时间琢磨了一下的地方。用户可能按月付、按季付、按年付,但仪表盘要展示「每月预计支出」,所以需要折算。我用了一个 BillingCycle enum 来统一处理:

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
        }
    }
}

有了这个 multiplier,任何订阅折算成月均成本只需要 price * multiplierToMonthly,仪表盘加总时直接用,逻辑干净。

货币这块也处理了一下国际化。AppSettings 里有个 defaultCurrencySymbol() 会读设备 Locale 推断默认货币符号,支持 CNY、USD、EUR、JPY、KRW 等十来个主要货币,不用用户手动选。

static func defaultCurrencySymbol() -> String {
    switch Locale.current.currency?.identifier ?? "CNY" {
    case "USD": return "$"
    case "EUR": return "€"
    case "JPY": return "JP¥"
    case "KRW": return "₩"
    case "TWD": return "NT$"
    default:    return "¥"
    }
}

看起来很小的细节,但如果默认给一个台湾用户显示 ¥,体验就很割裂。

订阅预设库:省掉用户手动填

录入订阅是最容易劝退用户的一步。我内置了一个 SubscriptionCatalog,收录了 Netflix、YouTube Premium、Disney+、Apple TV+、iCloud、ChatGPT Plus 等几十个常见服务,包含图标、建议周期、参考价格。

用户输入服务名称时会做模糊匹配(有 alias 列表,比如「youtube」能匹配到「YouTube Premium」),自动填充大部分字段,用户只需要确认价格和下次扣费日期就行。这个设计把录入一条订阅的时间压到了大概 20 秒。

有个功能我试了三个方案,全删了

早期我想做「自动识别银行短信/邮件里的扣费记录」,想着这样用户根本不用手动录入。试了三个思路:

  1. 解析通知权限读短信 → iOS 根本不给这个权限,死路
  2. 让用户截图,用 OCR 识别 → 准确率不稳定,而且不同银行格式差太多
  3. 做一个「分享账单截图进 App」的 Share Extension → 开发量大,而且用户粘性不够,不值得

最后全删了,回到「内置预设 + 快速录入」的路子。说实话有点可惜,但做产品就是这样,有些想法在纸上很美,实现起来代价太高。

OCR 功能保留了一个简化版——可以扫描账单截图里的金额数字,但不做全自动录入,只是辅助填数字。这个改成按次限量使用,AppSettings 里有 ocrUsageCountStorageocrUsageResetDateStorage 两个字段做频次控制,Pro 用户不限次,免费用户每月有额度。

仪表盘设计:让数字「疼」一点

纯数字没什么感觉。「每月订阅支出 287 元」和「每月订阅支出相当于你 4.3 小时的工资」,后者会让你真的停下来想一想。

所以 AppSettings 里存了用户的月薪和月均工时,仪表盘会把订阅支出折算成「工作时长」。这个设计灵感来自一个很老的观点:评估一件东西值不值,用它需要你工作多少小时来衡量,比直接看价格更直觉。

这个字段是可选的,不想填的用户跳过就行,不影响基础功能。

App 现状

1.1 版本刚上线不久,下载量还很少,没什么可拿出来说的数据。我自己用下来,每周会打开一两次,主要是看风险提醒有没有新的高危订阅冒出来。

功能上还有几个地方我打算继续做:iCloud 同步目前已经支持,但多设备的冲突处理逻辑还有点粗糙;订阅分类统计(流媒体、生产力工具、游戏等)的图表展示还比较简单,想做得更直观一点。

如果你也有订阅管理的经验,或者对风险检测的判断逻辑有什么想法,欢迎在评论区聊。我比较好奇大家觉得「高危订阅」的判定标准应该怎么设——现在我用的是「超过 60 天未使用 + 账单临近」这个组合,但这个阈值其实是我拍的,不一定对。