如何正确对SwiftUI StateObject进行依赖注入?
推荐解决方案:依赖容器 + 环境传递 + ViewModel工厂模式
该方案完全满足你提出的所有要求,实现简洁无额外第三方依赖,兼容Xcode 13/Swift 5.5的特性。
第一步:定义统一依赖容器
创建一个依赖容器类持有所有全局/局部依赖,本身遵循ObservableObject协议,方便沿视图树传递,同时内置工厂方法统一生成视图模型,避免重复编写初始化逻辑:
import SwiftUI class DIContainer: ObservableObject { // 支持同类型多实例共存,比如不同场景的HTTPClient let apiHTTPClient: HTTPClient let uploadDownloadHTTPClient: HTTPClient let secureStorage: SecureStorage let coreDataStorage: CoreDataStorage init( apiHTTPClient: HTTPClient, uploadDownloadHTTPClient: HTTPClient, secureStorage: SecureStorage, coreDataStorage: CoreDataStorage ) { self.apiHTTPClient = apiHTTPClient self.uploadDownloadHTTPClient = uploadDownloadHTTPClient self.secureStorage = secureStorage self.coreDataStorage = coreDataStorage } // 工厂方法:生成AuthViewModel,依赖自动注入 func makeAuthViewModel() -> AuthViewModel { AuthViewModel( httpClient: apiHTTPClient, secureStorage: secureStorage, coreDataStorage: coreDataStorage ) } // 后续新增其他视图模型,只需要在此处新增对应工厂方法即可 // func makeUserProfileViewModel() -> UserProfileViewModel { ... } }
第二步:在应用顶层初始化并注入容器
直接在@main结构体中完成所有依赖的初始化,避免全局单例的问题,将容器注入环境供下层视图使用:
@main struct MyApp: App { @StateObject private var diContainer = DIContainer( apiHTTPClient: HTTPClient(config: .apiConfig), uploadDownloadHTTPClient: HTTPClient(config: .uploadConfig), secureStorage: SecureStorage(), coreDataStorage: CoreDataStorage() ) var body: some Scene { WindowGroup { SplashScreen() .environmentObject(diContainer) } } }
第三步:在需要视图模型的页面完成初始化
利用Swift 5.5支持的@StateObject初始化特性,在View的初始化方法中通过容器的工厂方法生成视图模型,完全规避之前无法访问环境变量的问题:
struct LoginView: View { @EnvironmentObject private var diContainer: DIContainer @StateObject private var authViewModel: AuthViewModel // 直接通过构造参数传入容器,初始化视图模型 init(diContainer: DIContainer) { _authViewModel = StateObject(wrappedValue: diContainer.makeAuthViewModel()) } var body: some View { // 页面视图逻辑 } }
如果是通过路由跳转进入对应页面,只需要在跳转时传入环境中的容器即可:
// 上层跳转处的代码 NavigationLink("进入登录页") { LoginView(diContainer: diContainer) }
方案优势
- 完全符合你的要求:
- 所有依赖在
@main层统一初始化,沿视图树传递,所有视图模型都可以通过工厂方法获取依赖 - 视图模型本身不需要做任何修改,初始化要求的依赖全部为必填项,测试时直接传入Mock依赖即可,不需要改动内部逻辑
- 天然支持同类型多个依赖共存,比如示例中的两个不同配置的HTTPClient,只需要在工厂方法中指定对应实例即可
- 所有依赖在
- 编译时安全,不会出现依赖为空的情况,也不会出现开发者遗漏传入依赖的问题
- 扩展成本极低,后续新增视图模型只需要在依赖容器中加对应工厂方法即可,不需要修改其他逻辑
可选简化方案(适用于层级较深的应用)
如果你的应用层级非常深,不想层层传递容器参数,可以自定义SwiftUI环境键持有容器,不需要在View的初始化方法中传参:
// 定义自定义环境键 struct DIContainerKey: EnvironmentKey { static let defaultValue: DIContainer = DIContainer( apiHTTPClient: HTTPClient(config: .default), uploadDownloadHTTPClient: HTTPClient(config: .default), secureStorage: SecureStorage(), coreDataStorage: CoreDataStorage() ) } extension EnvironmentValues { var diContainer: DIContainer { get { self[DIContainerKey.self] } set { self[DIContainerKey.self] = newValue } } } // 注入时使用自定义环境键 SplashScreen() .environment(\.diContainer, diContainer) // 页面使用时不需要传构造参数 struct LoginView: View { @Environment(\.diContainer) private var diContainer @StateObject private var authViewModel: AuthViewModel? var body: some View { VStack { // 视图逻辑 } .onAppear { guard authViewModel == nil else { return } authViewModel = diContainer.makeAuthViewModel() } } }
内容的提问来源于stack exchange,提问作者Mai Anh Vu
相关产品推荐
相关产品推荐

