做了一个 iOS 订阅管理 App「订阅斩」,用 SwiftData 让「砍掉订阅」变成一件有爽感的事
背景:一笔 ¥68 的扣费引发的项目
上个月翻信用卡账单,发现一笔 ¥68 的扣费,愣了半天才想起来是去年试用的某个 PDF 工具自动续费了。不是因为钱多,而是完全不记得自己还在为它付费。
作为开发者,订阅制产品真的太多了:GitHub Copilot、JetBrains 全家桶、各种云服务、流媒体……每一笔单独看不多,加一起可能占月收入的 5%-10%。
所以我做了「订阅斩」——一个帮自己(也帮别人)管理和砍掉不必要订阅的 iOS App。目前已上架 App Store,版本 v1.2,从立项到上架花了大约 6 周业余时间。
技术选型:SwiftData + 纯 SwiftUI
项目从一开始就决定只支持 iOS 17+,因为我想用 SwiftData。
用过 Core Data 的都知道,NSManagedObject+NSFetchedResultsController+.xcdatamodeld那套写法有多啰嗦。SwiftData 用@Model宏直接标注数据类,配合 SwiftUI 的@Query属性包装器就能自动驱动 UI 刷新,开发体验提升非常明显。
核心数据模型:
@ModelfinalclassSubscription{varid:UUIDvarname:Stringvarprice:Doublevarcycle:Int// 0=月付, 1=季付, 2=年付varisTrial:BoolvarnextBillingDate:Datevarstatus:Int// active / killedvarkilledDate:Date?varisShared:BoolvarmembersCount:Intvaricon:StringvarunsubscribeURL:Stringvarsource:IntvarcategoryStorage:String?}``` 一个 `@Model` 类搞定持久化,不需要 `.xcdatamodeld` 文件,不需要手写 migration policy。至少在目前 v1.2的复杂度下,SwiftData完全够用。 ## 核心计算:统一换算月支出 订阅管理最基本的需求——用户录入的可能是月付 ¥15,也可能是年付 ¥298,需要统一展示「你每个月到底花了多少」。 ```swiftenumBillingCycle:Int,CaseIterable{casemonthly=0casequarterly=1caseyearly=2varmultiplierToMonthly:Double{switchself{case.monthly:1case.quarterly:1.0/3.0case.yearly:1.0/12.0}}}``` 仪表盘汇总时,把每条订阅的 `price*multiplierToMonthly` 求和就是月度总支出。逻辑简单,但很多用户之前从没认真算过这个数。有人录完发现自己每月光订阅就 ¥800+,当场就想取消几个。 ## 「斩」这个交互的设计思路 取消订阅在技术上就是把 `status` 从 active 改成 killed,再记录一个 `killedDate`。没什么复杂的。 但我花了不少时间在交互感受上。普通做法是一个"删除"或"归档"按钮,我把它包装成了「斩」:动词带攻击性,配合震动反馈(Haptics)和动画,让用户觉得自己在主动止损,而不是在做一个无聊的列表操作。App里有个 `unsubscribeCompletionCount` 计数器,记录用户总共「斩」了多少个订阅,后续计划做成就系统。目前 v1.2先埋了数据。 从用户反馈看,这个设计确实让「取消订阅」这件事变得没那么无聊了。 ## 风险检测:提醒你哪些订阅该斩了 我给每个订阅加了风险等级判断:-**高危**:试用期订阅+距离下次扣费 ≤3天(用户很可能忘了取消试用)--**中危**:超过60天没有打开对应服务(目前靠用户自己标记,后续考虑接ScreenTimeAPI)--**低危**:正常使用中的订阅 高危订阅在首页用红色标签高亮,配合本地通知在扣费前7天、3天、1天分别提醒。提醒时间可以在设置里逐个开关。 这个功能实际效果很好——试用期忘记取消是最常见的「冤枉钱」场景,提前提醒能直接帮用户省钱。 ## 降低录入门槛:预设库+OCR手动录入订阅名称、价格、周期,如果太麻烦用户就不会用。所以我做了一个预设库(`SubscriptionCatalog`),内置了几十个常见服务:Netflix、YouTubePremium、Spotify、iCloud+、Notion、GitHubCopilot等。 每个预设包含默认图标、推荐周期、参考价格、是否适合拼车、拼车人数等信息。用户输入名称时做模糊匹配(aliases 数组+字符串包含判断),命中就自动填充,录入时间从30秒缩短到5秒左右。 v1.2还加了一个实验功能:拍账单截图OCR识别订阅信息。做了免费次数限制(按月重置),体验还行但识别准确率不够理想,后面继续优化。 ## 一些技术取舍**iCloud 同步**:用SwiftData+CloudKit做的,默认开启。踩了一个坑——SwiftData的CloudKit同步在 iOS17.0-17.1有偶发的合并冲突问题,升到17.2之后基本稳定。加了手动开关让不信任云同步的用户可以关掉。**多币种**:用户可能有美元订阅(AppStore美区)、港币(AppleTV+港区)等,做了货币符号设置,默认根据设备 locale 推断,覆盖了USD/EUR/GBP/JPY/KRW/TWD/HKD等主要币种。**一个删掉的功能**:最初想做自动跳转到各平台取消订阅页面(存在 `unsubscribeURL` 里),实测发现大部分取消流程都需要登录态,跳过去用户还是得自己操作,引导意义有限。最后保留了URL字段但降低了展示优先级。 ## 给想做工具App的开发者一些建议 这个项目技术难度不高,SwiftData确实降低了不少数据层的工作量。 我觉得订阅管理这个品类最大的挑战不在技术,在于「用户录入意愿」。再好的功能,如果要用户花10分钟手动填20个订阅,大部分人第一步就放弃了。所以预设库、OCR、快速录入这些降低门槛的事情,比花哨的图表功能优先级高得多。 如果你也在做类似的个人工具项目,建议把「减少用户输入」排到 P0。---AppStore搜索「订阅斩」可以下载体验。如果你对SwiftData实践或者工具类App开发有想法,欢迎评论区交流。