SwiftUI 依赖图架构(对象生命周期与作用域)

发布: (2026年1月10日 GMT+8 07:11)
5 min read
原文: Dev.to

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 本身的问题——而是依赖图设计的问题。

Back to Blog

相关文章

阅读更多 »

SwiftUI #32:ProgressView

概述 ProgressView 创建一个进度条。初始化器 init_:value:total: 将标签作为第一个参数。value 表示当前进度……