我用 SwiftData 做了一个订阅管理 App,把每月「订阅刺客」揪出来

张开发
2026/4/27 6:30:31 15 分钟阅读

分享文章

我用 SwiftData 做了一个订阅管理 App,把每月「订阅刺客」揪出来
这个 App 是怎么来的说实话做这个 App 的起点挺俗的——信用卡账单。某个月翻账单的时候我发现有一笔扣费完全对不上号折腾了十几分钟才想起来三个月前试用了某个 SaaS 工具试用期结束自动续费了我压根没注意。金额不大但那种「钱不知不觉就没了」的感觉很难受。我去 App Store 找了一圈订阅管理工具要么太重预算管理全家桶那种要么太简陋就是个备忘录没有一个让我觉得「就是这个」。所以就自己做了一个。取名「订阅斩」SubKiller核心就一件事把你每个月到底在订阅上花了多少钱清清楚楚摆出来然后让你有仪式感地「斩掉」不需要的那些。产品逻辑为什么要有「斩」这个动作普通的订阅管理 App停用一个订阅就是点「删除」或者「归档」平淡无奇。我想了很久觉得这个动作应该有点重量感。毕竟你在主动切断一笔持续的资金流出这不是删一条备忘录这是一个决策。所以我把「停用订阅」这个操作包装成「斩」——有动画、有成就感、有一个专门的「已斩」Tab 存档你斩过的订阅。你随时可以进去看「我已经斩掉了 7 个订阅累计每月节省 ¥318」这个感觉完全不一样。数据模型怎么设计的App 用 SwiftData 做持久化核心模型是Subscription记录了名称、金额、计费周期、下次扣费日期、状态活跃/已斩、是否共享、退订渠道 URL 等字段。计费周期用了一个BillingCycle枚举monthly/quarterly/yearly三种换算成月均费用用multiplierToMonthly来做enumBillingCycle:Int,CaseIterable,Identifiable{casemonthly0casequarterly1caseyearly2varmultiplierToMonthly:Double{switchself{case.monthly:1case.quarterly:1.0/3.0case.yearly:1.0/12.0}}} 这样不管用户录入的是年费还是季费首页仪表盘展示「本月预计支出」的时候都能统一折算。---## 风险检测这个功能是后来加的 最开始版本没有风险检测只是纯粹记录和展示。 但我发现光「记录」还不够用。用户把订阅都录进去了但每个月也不一定会主动去翻。有些订阅放在那里半年没用过但因为不显眼就一直续费。 所以加了一个「风险等级检测」机制。判断逻辑是基于App里能实际感知到的信号-**高危**isTrialtrue 的试用期订阅、年费类订阅且距下次扣费 ≤7天且金额 ≥ ¥100--**中危**nextBillingDate 距今 ≤14天且该订阅自录入以来从未被手动「确认使用」过即 lastConfirmedDate 为nil或距今超过30天 换句话说我能感知的信号只有两类一是订阅本身的元数据金额、周期、下次扣费日二是用户在App内的主动操作有没有点过「我在用这个」。做不到检测用户有没有真的打开某个App所以高危判断完全基于账单维度不靠猜。 高危订阅会在首页仪表盘有明显提示用户能一眼看到「你有2个高危订阅需要审查」点进去就能直接斩。我自己用了两周斩掉了4个订阅一个是快忘了的设计工具年费一个是根本没在用的云存储每月节省将近 ¥200。---##SwiftData迁移这个坑必须说一下 AppSettings 里有几个字段是后来加的比如 remindBefore7Days、iCloudSyncEnabled。如果直接往 Model 里加字段旧版本用户升级之后SwiftData找不到对应列轻则数据读不出来重则直接崩。 我试了三个方案前两个手动 ModelMigrationPlan、用 Transient 绕过各有问题最后用的是「Optional字段originalName 标注计算属性兜底」这个组合 swiftModelfinalclassAppSettings{// 用 originalName 锁定持久化列名声明为 Optional 兼容旧 schemaAttribute(originalName:remindersEnabled)varremindersEnabledStorage:Bool?Attribute(originalName:iCloudSyncEnabled)variCloudSyncEnabledStorage:Bool?// 对外暴露的计算属性提供合理默认值varremindersEnabled:Bool{get{remindersEnabledStorage??true}set{remindersEnabledStoragenewValue}}variCloudSyncEnabled:Bool{get{iCloudSyncEnabledStorage??true}set{iCloudSyncEnabledStoragenewValue}}} Attribute(originalName:) 的作用是告诉SwiftData「持久化层的列名是这个但Swift属性名我可以随便改」配合 Optional 声明旧数据库里没有这列时不会崩直接返回nil然后计算属性给一个合理默认值。现在凡是新加字段我习惯性先想「旧数据怎么办」这个三件套基本能兜住大部分情况。---## 订阅预设库减少录入摩擦 录入订阅是最高频的操作如果每次都要手动填名字、金额、周期用两次就放弃了。 所以做了一个 SubscriptionCatalog内置了几十个常见订阅服务的预设包括Netflix、YouTubePremium、AppleTV、Spotify、各种国内流媒体和SaaS工具。用户输入名字时会模糊匹配命中预设之后自动填入推荐价格、计费周期、退订渠道。 每个预设里还有 sharedRecommended 字段标记这个服务是否适合家庭共享——如果是会提示用户「这个服务支持多人共享算下来人均更划算」。---## 当前状态App1.1版本已上架AppStoreID:6761400615支持 iOS 和 iPadOS。 上线3周DAU在40左右。目前最大的留存问题很明显用户录入第一个订阅之后就再也不回来了。首次使用的完整率还行但次日留存很低。我看来原因是App本身的「被动价值」不够强——不开App也不影响生活直到下次被订阅刺了一刀才会想起来。 接下来优先做两件事一是Widget让用户在桌面直接看到本月订阅支出制造每日曝光二是到期提醒通知在扣费前7天和3天推一条把用户拉回来做决策。AppSettings 里 remindBefore7Days 和 remindBefore3Days 的字段已经在了通知调度逻辑下个版本上。 对了ocrUsageCount 字段也已经在数据模型里占位——用OCR识别账单截图自动提取订阅信息这个功能打算在1.3做相关使用限额逻辑也预留了到时候单独写一篇。---## 最后 你现在每个月在订阅上花了多少钱这个数字大部分人真的说不出来不妨认真算一次可能会吓到自己。 如果你也在用SwiftData做 iOS 项目尤其是踩过 schema 迁移的坑欢迎在评论区聊——这块官方文档写得很薄踩坑经验互换比自己摸索快多了。

更多文章