SwiftUI中如何在ViewModel的init方法中使用EnvironmentObject?
SwiftUI中ViewModel优雅访问全局API实例的方案探讨
问题背景
我有一个基础API类:
class API: ObservableObject { @Published private(set) var isAccessTokenValid = false @AppStorage("AccessToken") var accessToken: String = "" @AppStorage("RefreshToken") var refreshToken: String = "" func request1() {} func request2() {} }
通过.environmentObject(API())将其全局传递给所有视图,视图可直接调用API发起请求。
但我有一个需要在视图出现时获取数据的ViewModel,其init()方法无法访问API实例:
class ViewModel: ObservableObject { @Published var data: [SomeResponseType] init() { // 无法在此处访问API实例来发起请求初始化data } } struct ViewA: View { @StateObject private var model = ViewModel() var body: View { VStack { // 使用model.data渲染UI } } }
我尝试了三种解决方案,但都觉得不符合SwiftUI风格:
现有解决方案
方案1:将API改为单例模式
class API: ObservableObject { static let shared = API() // 原有属性和方法... }
将全局注入改为.environmentObject(API.shared),ViewModel中直接用API.shared调用接口。
方案2:在视图的onAppear/task中直接调用请求
struct ViewA: View { @EnvironmentObject var api: API @State private var data: [SomeResponseType] = [] var body: View { VStack { // 使用data渲染UI } .task { let r = try? await api.request1() if let d = r { data = d } } } }
方案3:在onAppear/task中为ViewModel延迟配置API
class ViewModel: ObservableObject { @Published var data: [SomeResponseType] = [] private var api: API! func setup(api: API) { self.api = api } func fetchData() async { let r = try? await api.request1() if let d = r { data = d } } } struct ViewA: View { @EnvironmentObject var api: API @StateObject private var model = ViewModel() var body: View { VStack { // 使用model.data渲染UI } .task { model.setup(api) await model.fetchData() } } }
更符合SwiftUI风格的优化方案
1. 显式依赖注入(推荐)
这是现代iOS开发的标准实践,让ViewModel的依赖清晰可见,同时方便单元测试时替换Mock实例。
实现方式
修改ViewModel和视图,让API通过构造函数传入:
class ViewModel: ObservableObject { @Published var data: [SomeResponseType] = [] private let api: API // 用不可变常量确保依赖稳定 init(api: API) { self.api = api // 初始化时异步请求数据 Task { await fetchData() } } func fetchData() async { do { data = try await api.request1() } catch { // 处理请求错误 print("请求失败:\(error.localizedDescription)") } } } struct ViewA: View { @EnvironmentObject private var api: API @StateObject private var model: ViewModel // 通过视图构造函数注入API,初始化ViewModel init(api: API? = nil) { let resolvedAPI = api ?? API() _model = StateObject(wrappedValue: ViewModel(api: resolvedAPI)) } var body: View { VStack { List(model.data) { item in Text(item.title) } } } } // 在App入口全局配置API @main struct MyApp: App { @StateObject private var api = API() var body: some Scene { WindowGroup { ViewA(api: api) .environmentObject(api) } } }
进阶:使用EnvironmentKey简化注入
如果不想手动在每个视图传递API,可以自定义EnvironmentKey,让视图直接从环境中获取:
// 定义EnvironmentKey struct APIEnvironmentKey: EnvironmentKey { static let defaultValue: API = API() } extension EnvironmentValues { var api: API { get { self[APIEnvironmentKey.self] } set { self[APIEnvironmentKey.self] = newValue } } } // 修改视图写法 struct ViewA: View { @Environment(\.api) private var api @StateObject private var model: ViewModel init() { _model = StateObject(wrappedValue: ViewModel(api: APIEnvironmentKey.defaultValue)) } var body: View { VStack { // UI内容 } .onAppear { // 若需要在视图出现时才请求,可在此调用model.fetchData() } } } // App中配置环境 @main struct MyApp: App { @StateObject private var api = API() var body: some Scene { WindowGroup { ViewA() .environment(\.api, api) } } }
2. 拆分API职责:状态与请求分离
当前API类混合了认证状态管理和网络请求逻辑,违反单一职责原则。拆分后代码更清晰,也更符合SwiftUI的状态管理理念:
实现方式
// 纯网络请求客户端,无状态,仅负责发送请求 struct APIClient { let accessToken: String let refreshToken: String func request1() async throws -> [SomeResponseType] { // 实现具体请求逻辑,使用accessToken鉴权 } func refreshToken() async throws -> (newAccessToken: String, newRefreshToken: String) { // 实现token刷新逻辑 } } // 认证管理器,负责状态管理和token自动刷新 class AuthManager: ObservableObject { @Published private(set) var isAccessTokenValid = false @AppStorage("AccessToken") private var accessToken: String = "" @AppStorage("RefreshToken") private var refreshToken: String = "" private var apiClient: APIClient { APIClient(accessToken: accessToken, refreshToken: refreshToken) } init() { validateToken() } private func validateToken() { isAccessTokenValid = !accessToken.isEmpty && !refreshToken.isEmpty } // 封装带自动重试的请求方法 func performRequest<T>(_ request: (APIClient) async throws -> T) async throws -> T { do { return try await request(apiClient) } catch APIError.tokenExpired { // 刷新token并重试 let (newAT, newRT) = try await apiClient.refreshToken() accessToken = newAT refreshToken = newRT validateToken() return try await request(apiClient) } } // 暴露对外的请求方法 func request1() async throws -> [SomeResponseType] { try await performRequest { $0.request1() } } }
然后ViewModel注入AuthManager即可:
class ViewModel: ObservableObject { @Published var data: [SomeResponseType] = [] private let authManager: AuthManager init(authManager: AuthManager) { self.authManager = authManager Task { await fetchData() } } func fetchData() async { do { data = try await authManager.request1() } catch { print("请求失败:\(error)") } } } // 视图中使用 struct ViewA: View { @EnvironmentObject private var authManager: AuthManager @StateObject private var model: ViewModel init(authManager: AuthManager? = nil) { let resolvedAuth = authManager ?? AuthManager() _model = StateObject(wrappedValue: ViewModel(authManager: resolvedAuth)) } var body: View { VStack { if authManager.isAccessTokenValid { List(model.data) { item in Text(item.name) } } else { Text("请先登录") } } } }
对原有三种方案的点评
- 方案1(单例):实现简单,但耦合度高,难以进行单元测试,不符合依赖注入的设计理念,后续扩展维护难度大。
- 方案2(视图中处理请求):将业务逻辑放在视图中,违反MVVM模式的职责划分,代码复用性差,视图会变得臃肿。
- 方案3(延迟注入):ViewModel存在未初始化的API风险,破坏了对象初始化的完整性,容易引发空值异常。
内容的提问来源于stack exchange,提问作者FaiChou
相关产品推荐
相关产品推荐

