SwiftUI 依赖图架构(对象生命周期与作用域)
Source: Dev.to
🧠 核心原则
如果你不设计对象的生命周期,SwiftUI 会为你设计,而你不会喜欢结果。
每个对象必须拥有:
- 明确的所有者
- 明确的生命周期
- 明确的作用域
🧱 1. 你必须建模的三种生命周期
每个依赖都属于以下类别之一:
1. 应用程序生命周期
- analytics
- feature flags
- auth session
- configuration
- logging
2. 特性生命周期
- ViewModels
- repositories
- coordinators
- use cases
3. 视图生命周期
- ephemeral helpers
- formatters
- local state
混合使用这些生命周期会导致内存泄漏和错误。
🧭 2. 依赖图层
分层思考:
AppContainer
↓
FeatureContainer
↓
ViewModel
↓
View
数据和所有权仅向 下游 流动;不应向上流动。
🏗️ 3. 应用容器(图的根)
final class AppContainer {
let apiClient: APIClient
let authService: AuthService
let analytics: AnalyticsService
let featureFlags: FeatureFlagService
init() {
self.apiClient = APIClient()
self.authService = AuthService()
self.analytics = AnalyticsService()
self.featureFlags = FeatureFlagService()
}
}
- 只创建一次
- 在整个应用生命周期内存在
- 向下注入
- 绝不重新创建此实例。
📦 4. 功能容器(作用域生命周期)
每个功能都会构建自己的图:
final class ProfileContainer {
let repository: ProfileRepository
let viewModel: ProfileViewModel
init(app: AppContainer) {
self.repository = ProfileRepository(api: app.apiClient)
self.viewModel = ProfileViewModel(repo: repository)
}
}
- 在功能出现时创建
- 在功能消失时销毁
- 拥有其
ViewModel
这为你提供了干净的拆卸。
🧩 5. ViewModels 不构建依赖
Bad:
class ProfileViewModel {
let api = APIClient()
}
Good:
class ProfileViewModel {
let repo: ProfileRepository
init(repo: ProfileRepository) {
self.repo = repo
}
}
ViewModels 消费 依赖;它们永不构造这些依赖。
🧬 6. 将环境用作图注入器(而非存储)
通过环境传递容器,而不是单独的服务:
.environment(\.profileContainer, container)
在视图中获取它们:
@Environment(\.profileContainer) var container
视图在 不 依赖全局状态的情况下获取其 ViewModel 和其他依赖。
🧱 7. 生命周期 = 所有者
如果你无法回答“谁来释放它?”那就是一个 bug。
所有权示例:
AppContainer→ 应用生命周期FeatureContainer→ 导航生命周期ViewModel→ 功能生命周期View→ 页面生命周期
所有权必须在代码中可见。
🔄 8. 导航定义对象生命周期
导航是内存管理,而不仅仅是 UI。
NavigationStack(path: $path) {
FeatureEntry()
}
当功能离开堆栈时:
- 它的容器被释放
- 它的
ViewModel被释放 - 订阅被取消
- 任务停止
如果没有发生上述情况,图结构就是错误的。
🧠 9. 避免单例(不失便利性)
而不是:
Analytics.shared.track()
应该:
analytics.track()
其中 analytics 是从 AppContainer 注入的。
你可以保持:
- 类全局访问
- 可测试性
- 可控性
而不使用真正的全局状态。
🧪 10. 测试图
因为所有东西都是注入的,测试非常直接:
let mockRepo = MockProfileRepository()
let vm = ProfileViewModel(repo: mockRepo)
无需:
- 存根全局变量
- 重写单例
- 与系统对抗
你的依赖图 就是 你的测试工具。
❌ 11. 常见反模式
避免:
- 在 ViewModel 中构建服务
- 全局静态单例
- 到处注入所有东西
- 生命周期超出导航的特性容器
- 循环依赖
- 将 environment 对象用作服务定位器
这些会导致内存泄漏、错误、不可测试的代码以及无法进行的重构。
🧠 心智模型
Who creates it?
Who owns it?
Who destroys it?
如果你能回答这三个问题,你的图是健康的;否则,你就有架构债务。
🚀 最终思考
干净的依赖图能够:
- 可预测的内存使用
- 干净的资源释放
- 便捷的测试
- 更安全的重构
- 更快的上手
- 更少的生产环境 Bug
它是以下内容的基石:
- 模块化架构
- 多平台应用
- 大型团队
- 长期维护的代码库
大规模的 SwiftUI 问题往往不是 SwiftUI 本身的问题——而是依赖图设计的问题。