You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确对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)
}

方案优势

  1. 完全符合你的要求:
    • 所有依赖在@main层统一初始化,沿视图树传递,所有视图模型都可以通过工厂方法获取依赖
    • 视图模型本身不需要做任何修改,初始化要求的依赖全部为必填项,测试时直接传入Mock依赖即可,不需要改动内部逻辑
    • 天然支持同类型多个依赖共存,比如示例中的两个不同配置的HTTPClient,只需要在工厂方法中指定对应实例即可
  2. 编译时安全,不会出现依赖为空的情况,也不会出现开发者遗漏传入依赖的问题
  3. 扩展成本极低,后续新增视图模型只需要在依赖容器中加对应工厂方法即可,不需要修改其他逻辑

可选简化方案(适用于层级较深的应用)

如果你的应用层级非常深,不想层层传递容器参数,可以自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 00:39:01