日均的技术栈很简单:SwiftUI + SwiftData,纯 iOS 原生开发。没有后端,没有第三方依赖,数据通过 iCloud 同步。
这篇记录一些技术选型的思考和实际开发中的经验。
为什么选 SwiftUI + SwiftData
2026 年做一个新的 iOS App,SwiftUI 已经是默认选择。但 SwiftData 的选择需要多想一步。
选 SwiftData 的理由:
- 与 SwiftUI 深度集成,
@Query宏让数据绑定极其简洁 - 内置 iCloud 同步(CloudKit),零配置
- 不需要写 migration 代码(对于 v1.x 阶段的快速迭代很重要)
- 一个人开发,学习成本要低
放弃 Core Data 的理由:
- 样板代码太多
- NSManagedObject 和 SwiftUI 的配合不够自然
- 2026 年了,没必要背历史包袱
没考虑 Realm / SQLite 的理由:
- 引入第三方依赖 = 引入维护风险
- 日均的数据模型很简单,不需要复杂查询
- iCloud 同步是刚需,SwiftData 原生支持
数据模型设计
日均的核心模型只有两个:
@Model
class TrackedItem {
var name: String
var emoji: String
var price: Double
var trackingType: TrackingType // byTime or byCount
var purchaseDate: Date
var expectedUsageDays: Int?
var usageRecords: [UsageRecord]
var isTracked: Bool // prevent duplicate tracking from calm list
// computed: dailyCost, costPerUse, totalUses...
}
@Model
class CalmItem {
var name: String
var emoji: String
var price: Double
var calmDays: Int // 3, 5, 7, or 14
var addedDate: Date
var isTracked: Bool
// computed: expiryDate, daysRemaining...
}
设计原则:模型尽量扁平,计算属性代替冗余字段。
比如 dailyCost 不存储在数据库里,而是每次从 price、purchaseDate、usageRecords 实时计算。好处是数据永远一致,坏处是列表多的时候可能有性能问题——但日均的数据量级(几十到几百个物品)完全不是问题。
iCloud 同步:零配置的代价
SwiftData + CloudKit 的 iCloud 同步确实是"零配置"——在 Xcode 里勾选 iCloud 能力,选择 CloudKit 容器,就完了。
但"零配置"不等于"零问题":
- 同步延迟不可控 — 有时候几秒,有时候几分钟。用户换设备后可能看不到最新数据。
- 冲突解决是黑盒 — CloudKit 的 last-write-wins 策略,你无法自定义。
- 调试困难 — 同步出问题时,几乎没有日志可看。
对日均来说,这些问题可以接受。用户不会在两台设备上同时编辑同一个物品,同步延迟几分钟也不影响使用。
Widget 开发:App Group 是关键
v1.5 加入了桌面小组件。Widget Extension 是独立进程,不能直接访问主 App 的 SwiftData 数据库。
解决方案:App Group + UserDefaults 缓存。
主 App 数据变更 → 写入 App Group UserDefaults → Widget 读取缓存
为什么不让 Widget 直接访问 SwiftData?
- Widget 的内存限制很严格
- SwiftData 的初始化开销不小
- Widget 只需要展示数据,不需要写入
实际做法:主 App 在每次数据变更时,把 Widget 需要的数据(日均成本最低的物品、冷静清单倒计时)序列化写入 UserDefaults。Widget 只读这个缓存。
StoreKit 2 付费:写好但不上线
日均的付费逻辑已经写好了:
class PurchaseManager {
// 月付 ¥3 / 年付 ¥18 / 永久买断 ¥38
func purchase(_ product: Product) async throws -> Transaction
func restorePurchases() async
var isPro: Bool { get }
}
StoreKit 2 比 StoreKit 1 好用太多——纯 Swift async/await API,不需要处理 SKPaymentQueue 的回调地狱。
但这些代码目前处于"写好不上线"状态。PaywallView 写好了,产品 ID 定义好了,但 App Store Connect 里没有配置产品。原因在留存那篇文章里说过:留存没达标之前,上付费没有意义。
通知系统:一个低级 Bug 的教训
v1.2 的冷静清单有一个严重 bug:到期通知从未触发。
原因很简单:scheduleCalmExpiry 函数写好了,但在添加冷静物品的流程里忘了调用。
// v1.2 的代码(有 bug)
func addCalmItem(_ item: CalmItem) {
modelContext.insert(item)
// scheduleCalmExpiry(item) ← 这行不存在
}
// v1.3.1 修复后
func addCalmItem(_ item: CalmItem) {
modelContext.insert(item)
scheduleCalmExpiry(item)
requestNotificationPermission() // 同时请求权限
}
教训:写完功能要走一遍完整的用户流程。不是测试单个函数,而是从"用户点击添加"到"7 天后收到通知"的完整链路。
HTML 原型先行
日均的开发流程有一个不太常见的做法:先用 HTML 做原型,再写 SwiftUI。
ui/ 目录下有完整的 HTML 原型:
page1_home.html— 首页列表page2_add.html— 添加物品page3_detail.html— 物品详情page4_calm_list.html— 冷静清单page5_calm_detail.html— 冷静详情page6_insight.html— 洞察页
为什么用 HTML 而不是 Figma?
- 我更熟悉 CSS — 写 HTML 比拖 Figma 快
- 可以精确定义数值 — 颜色、间距、圆角都是代码,直接复制到 SwiftUI
- 可以在浏览器里交互 — 比静态设计稿更接近真实体验
- 零成本 — 不需要 Figma 订阅
项目结构
日均的代码组织很简单:
ZhiValue/Sources/ZhiValue/
├── Models/ # SwiftData 模型
├── Views/ # SwiftUI 视图
│ ├── Calculator/ # 算一算
│ ├── Items/ # 物品列表和详情
│ ├── Calm/ # 冷静清单
│ ├── Insight/ # 洞察
│ └── Settings/ # 设置
├── Services/ # 通知、购买等服务
└── Theme/ # AppTheme(颜色、字体、间距)
没有用 MVVM、没有用 Coordinator、没有用 Clean Architecture。对于一个人开发的小 App,View 直接操作 Model 是最简单高效的方式。
等 App 复杂到需要架构的时候再重构,不要提前过度设计。
给独立开发者的技术建议
- SwiftData 够用就用 — 除非你需要复杂查询或自定义同步逻辑
- 不要引入不必要的依赖 — 每个依赖都是维护负担
- HTML 原型比 Figma 更适合开发者 — 如果你会写 CSS 的话
- 先跑通完整流程,再优化细节 — 通知 bug 就是反面教材
- 代码写好不等于要上线 — 付费逻辑可以提前写,但上线时机要看数据
- 计算属性优于冗余字段 — 数据一致性比性能更重要(在小数据量下)
这是日均系列的最后一篇。五篇文章覆盖了产品定位、设计系统、留存优化、获客漏斗和技术实践——一个独立开发者从 0 到 1 的完整记录。
日均还在继续迭代。如果你也在做独立开发,欢迎在 App Store 搜索「日均」体验,或者在小红书找到我聊聊。