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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 20:55:23