【问题标题】:SwiftUI - How to pass EnvironmentObject into View Model?SwiftUI - 如何将 EnvironmentObject 传递给视图模型?
【发布时间】:2020-04-16 21:19:10
【问题描述】:

我希望创建一个可由视图模型(不仅仅是视图)访问的 EnvironmentObject。

Environment 对象跟踪应用程序会话数据,例如登录、访问令牌等,这些数据将被传递到视图模型(或需要的服务类)中,以允许调用 API 来传递来自此 EnvironmentObjects 的数据。

我尝试将会话对象从视图传递给视图模型类的初始化程序,但出现错误。

如何使用 SwiftUI 访问/传递 EnvironmentObject 到视图模型中?

【问题讨论】:

  • 为什么不将 viewmodel 作为 EO 传递?
  • 好像过头了,会有很多视图模型,我链接的上传只是一个简化的例子
  • 我不知道为什么这个问题被否决了,我也想知道。我会用我所做的来回答,希望其他人可能会想出更好的东西。
  • @E.Coms 我希望 EnvironmentObject 通常是一个对象。我知道多项工作,让它们像这样在全球范围内访问似乎是一种代码味道。
  • @Michael 你找到解决方案了吗?

标签: ios swift mvvm swiftui


【解决方案1】:

以下提供的方法对我有用。从 Xcode 11.1 开始使用许多解决方案进行测试。

问题源于EnvironmentObject在视图中注入的方式,一般模式

SomeView().environmentObject(SomeEO())

即,第一次创建视图,第二次创建环境对象,第三次环境对象注入视图

因此,如果我需要在视图构造函数中创建/设置视图模型,环境对象还不存在。

解决方案:将所有内容分开并使用显式依赖注入

这是它在代码中的样子(通用架构)

// somewhere, say, in SceneDelegate

let someEO = SomeEO()                            // create environment object
let someVM = SomeVM(eo: someEO)                  // create view model
let someView = SomeView(vm: someVM)              // create view 
                   .environmentObject(someEO)

这里没有任何权衡,因为 ViewModel 和 EnvironmentObject 在设计上是引用类型(实际上是ObservableObject),所以我在这里和那里只传递引用(又名指针)。

class SomeEO: ObservableObject {
}

class BaseVM: ObservableObject {
    let eo: SomeEO
    init(eo: SomeEO) {
       self.eo = eo
    }
}

class SomeVM: BaseVM {
}

class ChildVM: BaseVM {
}

struct SomeView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: SomeVM

    init(vm: SomeVM) {
       self.vm = vm
    }

    var body: some View {
        // environment object will be injected automatically if declared inside ChildView
        ChildView(vm: ChildVM(eo: self.eo)) 
    }
}

struct ChildView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: ChildVM

    init(vm: ChildVM) {
       self.vm = vm
    }

    var body: some View {
        Text("Just demo stub")
    }
}

【讨论】:

  • 我刚开始使用 MVVM,这是最接近我想做的事情。我很惊讶我无法在我的 ObservableObject ViewModel 中访问我的 EnvironmentObjects。我唯一不喜欢的是视图模型在 SceneDelegate 或父视图中暴露,我认为这不太正确。在视图中创建视图模型对我来说更有意义。但是目前我没有看到解决此问题的方法,您的解决方案是迄今为止最好的。
  • 因此,一方面对于视图,我们可以实现传递依赖项的环境对象样式,另一方面对于 ViewModels,我们需要将其传递到链中(SwiftUI 试图通过引入 EnvironmentObjects 来避免)
  • 在您的SomeView 中,您的虚拟机声明是否应该是@StateObject 而不是@ObservedObject
  • @Asperi - 这是一个非常好的模式。您是否设法将其调整为与@StateObjects 一起使用?我收到一个错误,因为它们似乎是一个只能获取的属性。
【解决方案2】:

你可以这样做:

struct YourView: View {
  @EnvironmentObject var settings: UserSettings

  @ObservedObject var viewModel = YourViewModel()

  var body: some View {
    VStack {
      Text("Hello")
    }
    .onAppear {
      self.viewModel.setup(self.settings)
    }
  }
}

对于 ViewModel:

class YourViewModel: ObservableObject {
  
  var settings: UserSettings?
  
  func setup(_ settings: UserSettings) {  
    self.settings = settings
  }
}

【讨论】:

  • 这病了..有什么缺点吗?不管我怎么想.. 我的 swiftui 代码总是以 MVVM 结尾,这是最自然和结构化的!
  • 暂时没有看到任何缺点......它工作得很好,我正在使用它来更改 viewModel 中的选项卡
  • 不利的一面是你总是会有可选的。
  • 另一个缺点是您在设置中的更新不会自动传达给查看,因为您会失去 ObservableObject 和 EnvironmentObject 的灵活性。
  • 我观察到 onAppear 在视图显示后被调用。因此,如果您需要使用 viewModel 中的某些逻辑和设置,您将无法获得它。
【解决方案3】:

你不应该。 SwiftUI 最适合 MVVM 是一个常见的误解。 MVVM 在 SwiftUI 中没有位置。您在问是否可以推动矩形以适应三角形。不合适。

让我们从一些事实开始,一步一步地工作:

  1. ViewModel 是 MVVM 中的模型。

  2. MVVM 不考虑值类型(例如;Java 中没有这样的东西)。

  3. 在不变性的意义上,值类型模型(没有状态的模型)被认为比引用类型模型(有状态的模型)更安全。

现在,MVVM 要求您以这样一种方式设置模型,即每当模型发生变化时,它都会以某种预先确定的方式更新视图。这称为绑定。

没有绑定,您将无法很好地分离关注点,例如;重构模型和相关状态,并将它们与视图分开。

这是大多数 iOS MVVM 开发人员失败的两件事:

  1. iOS 没有传统 Java 意义上的“绑定”机制。有些人会忽略绑定,并认为调用对象 ViewModel 会自动解决所有问题;有些人会引入基于 KVO 的 Rx,并在 MVVM 应该让事情变得更简单时使一切变得复杂。

  2. 有状态的模型太危险了,因为 MVVM 太强调 ViewModel,太少强调状态管理和管理控制的一般规则;大多数开发人员最终认为具有用于更新视图的状态的模型是可重用可测试。这就是 Swift 首先引入值类型的原因;没有状态的模型。

现在你的问题是:你问你的 ViewModel 是否可以访问 EnvironmentObject (EO)?

你不应该。因为在 SwiftUI 中,符合 View 的模型会自动引用 EO。例如;

struct Model: View {
    @EnvironmentObject state: State
    // automatic binding in body
    var body: some View {...}
}

我希望人们能够体会到 SDK 是如何设计的。

在 SwiftUI 中,MVVM 是自动的。不需要手动绑定到需要传递给它的 EO 引用的视图的单独 ViewModel 对象。

上面的代码 MVVM。例如。;绑定到视图的模型。但是因为模型是值类型,所以不是将模型和状态重构为视图模型,而是重构出控制(例如在协议扩展中)。

这是官方 SDK 使设计模式适应语言特性,而不仅仅是强制执行。实质重于形式。看看你的解决方案,你必须使用基本上是全局的单例。您应该知道在没有不变性保护的情况下在任何地方访问全局是多么危险,因为您必须使用引用类型模型,所以您没有这种保护!

TL;DR

您不会在 SwiftUI 中以 java 方式执行 MVVM。而且 Swift-y 的做法是不需要这样做,它已经内置了。

希望更多开发人员看到这个,因为这似乎是一个热门问题。

【讨论】:

  • "ViewModel 是 MVVM 中的模型。"不,ViewModel 是 MVVM 中的视图模型。模型和视图是其他实体。将 MVVM 与 SwiftUI 一起使用非常好。
  • “不,ViewModel 是 MVVM 中的视图模型”。这是counter example
  • 那么,如果不使用视图模型,您将如何使用数据任务发布者通过服务加载数据以显示在视图中?
  • 专为满足项目需求而定制的网络服务对象。将网络与视图或模型分离。有一个普遍的误解是你必须使用“视图模型”来做“网络”。不,您使用网络来进行网络。例如。; resource.post(params).onSuccess { json in self.data = json} 然后使用属性观察器进行模型视图更新,例如; var data = JSON() { didSet { updateUI() }}。这些都不需要视图模型的概念,但它做同样的事情。这是没有 VM 的 MVVM,或将 MVVM 适配为 Swift 语言特性。
  • 我看到很多人在这里询问 SO 很难完成某件事,然后显示了将所有内容混合到一个 SwiftUI 视图中的复杂代码。我们可以做到这一点,甚至像从 UITableViewCell 调用 Core Data 这样尴尬的事情,都是众所周知的事实。但是 MVVM 确实定义了分离和组件是有原因的。您可以在 SwiftUI 中将 ELM 架构实现为单个视图,只需 30 行干净漂亮的代码即可支持您的想法 - 最好让它可测试、可依赖注入,这需要您接受一些分离的组件。
【解决方案4】:

我选择没有 ViewModel。 (也许是时候换一种新模式了?)

我已经使用RootView 和一些子视图设置了我的项目。我将RootView 设置为使用App 对象作为EnvironmentObject。我的所有视图都访问 App 上的类,而不是 ViewModel 访问模型。不是 ViewModel 决定布局,而是视图层次结构决定布局。通过在一些应用程序的实践中这样做,我发现我的观点保持小而具体。作为过度简化:

class App: ObservableObject {
   @Published var user = User()

   let networkManager: NetworkManagerProtocol
   lazy var userService = UserService(networkManager: networkManager)

   init(networkManager: NetworkManagerProtocol) {
      self.networkManager = networkManager
   }

   convenience init() {
      self.init(networkManager: NetworkManager())
   }
}
struct RootView: View {
    @EnvironmentObject var app: App
    
    var body: some View {
        if !app.user.isLoggedIn {
            LoginView()
        } else {
            HomeView()
        }
    }
}
struct HomeView: View {
    @EnvironmentObject var app: App

    var body: some View {
       VStack {
          Text("User name: \(app.user.name)")
          Button(action: { app.userService.logout() }) {
             Text("Logout")
          }
       }
    }
}

在我的预览中,我初始化了一个MockApp,它是App 的一个子类。 MockApp 使用 Mocked 对象初始化指定的初始化程序。这里不需要对 UserService 进行模拟,但对数据源(即 NetworkManagerProtocol)进行模拟。

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            HomeView()
                .environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
        }
    }

}

【讨论】:

  • App 应该派生自 ObservableObject
  • 你也可以使用单个初始化:init(networkManager: NetworkManagerProtocol = NetworkManager()) {
  • 虽然这种模式一开始很诱人,但所有依赖于 App 的视图都会在 App 发生变化时立即刷新,即使给定的视图没有观察到刚刚更新的特定属性。这对您有伤害吗?如果有,您是否找到了缓解这种情况的方法?
  • @pommefrite 我从来没有遇到过这个问题,而且我已经分析了我所有的应用程序,因为 SwiftUI 中有很多效率低下的地方,只有 Instruments 才能看到。我看不出它是个问题,因为动画不是用 EnvironmentObjects 完成的,而且视图函数只返回一个 Apple 专门为 SwiftUI 优化的结构。
【解决方案5】:

Resolver 库在为模型类获取依赖注入方面做得很好。它提供了一个属性包装器@Injected,它在精神上与@EnvironmentObject 非常相似,但适用于任何地方。所以在一个模型中,我会像这样注入一个 ExampleService:

class ExampleModel: ObservableObject {

    @Injected var service: ExampleService

    // ...
}

这也可以用来解决视图的依赖关系:

struct ExampleView: View {

    @ObservedObject var exampleModel: ExampleModel = Resolver.resolve()

    var body: some View {
        // ...
    }
}

Views 的另一种选择是在 SwiftUI 视图层次结构中使用 @EnvironmentObject,但这有点麻烦,因为您将有两个依赖注入容器,Resolver/@Injected 用于应用程序范围/服务的所有内容和视图层次结构中的 SwiftUI/@EnvironmentObject 用于与视图/视图模型相关的所有内容。

【讨论】:

  • 我喜欢Resolver 的概念,并且我可以看到能够将应用程序范围内的注入不仅注入视图而且注入模型的好处。但是,再三考虑,我不喜欢依赖第 3 方解决方案。难道没有一个很好的 SwiftUI-Combine-only 方式吗?
【解决方案6】:

这是我发现在 viewModel 中访问和更新 @EnvironmentObject 属性的最简单方法:

// ContentView.swift

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var store: Store

    var body: some View {
        Child(viewModel: ChildViewModel(store))
    }
}

// Child.swift

import SwiftUI

struct Child: View {
    // only added here to verify that the actual
    // @EnvironmentObject store was updated
    // not needed to run
    @EnvironmentObject var store: Store

    @StateObject var viewModel: ViewModel

    var body: some View {
        Text("Hello, World!").onAppear {
            viewModel.update()
            print(store.canUpdateStore)
            // prints true
        }
    }
}

extension Child {
    final class ViewModel: ObservableObject {
        let store: StoreProtocol

        init(store: StoreProtocol) {
            self.store = store
        }

        public func update() {
            store.updateStore()
        }
    }
}


// myApp.swift

import SwiftUI

protocol StoreProtocol {
    var canUpdateStore: Bool { get }
    func updateStore() -> Void
}

class Store: ObservableObject, StoreProtocol {
    @Published private(set) var canUpdateStore: Bool = false

    func updateStore() {
        canUpdateStore = true
    }
}

@main
struct myApp: App {
    @StateObject private var store = Store()

    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(store)
        }
    }
}

这种方法还允许您在单元测试ChildViewModel 或在画布预览中通过依赖注入模拟store

与使用 onAppear 的其他 hacky 方法不同,没有可选选项可以在触发 onAppear 之前运行代码,并且视图模型的范围仅限于它所服务的视图。

您也可以直接在 viewModel 中修改 store,这也很好。

【讨论】:

  • 如果您在将 @StateObject 传递给初始化程序时创建它,它将每次都重新创建,这违背了目的。如果你手动编写初始化器,编译器会警告你。
【解决方案7】:

也许这或多或少是关于观点的:

// ViewModel 
struct ProfileViewModel {
    @EnvironmentObject state: State
    private func businessLogic() {}
}

// The "separate" UI part of the view model
extension ProfileViewModel: View {
    var body: some View {
        ProfileView(model: self)
    }
}

// The "real" view
struct ProfileView: View {
    @ObservedObject var model
    @Environment(\.accessibilityEnabled) var accessibilityEnabled 
    var body: some View {
        // real view
    }
}

【讨论】:

    猜你喜欢
    • 2011-08-10
    • 2020-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-23
    • 2014-02-05
    • 1970-01-01
    相关资源
    最近更新 更多