SwiftUI - 如何将 EnvironmentObject 传递到视图模型中?

Mic*_*ael 70 mvvm ios swift swiftui

我正在寻找创建一个可以由视图模型(不仅仅是视图)访问的 EnvironmentObject。

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

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

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

eme*_*hex 15

解决方案:iOS 14/15+

以下是您可以如何与视图模型中的环境对象进行交互,而无需在实例化时注入它:

  1. 定义环境对象:
import Combine

final class MyAuthService: ObservableObject {
    @Published private(set) var isSignedIn = false
    
    func signIn() {
        isSignedIn = true
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 创建一个视图来拥有并传递环境对象:
import SwiftUI

struct MyEntryPointView: View {
    @StateObject var auth = MyAuthService()
    
    var body: some View {
        content
            .environmentObject(auth)
    }
    
    @ViewBuilder private var content: some View {
        if auth.isSignedIn {
            Text("Yay, you're all signed in now!")
        } else {
            MyAuthView()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 使用将环境对象作为参数的方法定义视图模型:
extension MyAuthView {
    @MainActor final class ViewModel: ObservableObject {
        func signIn(with auth: MyAuthService) {
            auth.signIn()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 创建一个拥有视图模型、接收环境对象并调用适当方法的视图:
struct MyAuthView: View {
    @EnvironmentObject var auth: MyAuthService
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        Button {
            viewModel.signIn(with: auth)
        } label: {
            Text("Sign In")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 预览它的完整性:
struct MyEntryPointView_Previews: PreviewProvider {
    static var previews: some View {
        MyEntryPointView()
    }
}
Run Code Online (Sandbox Code Playgroud)


Asp*_*eri 13

下面提供了对我有用的方法。测试了许多从 Xcode 11.1 开始的解决方案。

问题源于在view中注入EnvironmentObject的方式,一般schema

SomeView().environmentObject(SomeEO())
Run Code Online (Sandbox Code Playgroud)

即,在第一个创建的视图中,在第二个创建的环境对象中,在注入到视图中的第三个环境对象中

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

解决方案:分解一切并使用显式依赖注入

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

SomeView().environmentObject(SomeEO())
Run Code Online (Sandbox Code Playgroud)

这里没有任何权衡,因为 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")
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我刚刚开始使用 MVVM,这是最接近我想做的事情。我很惊讶我无法访问 ObservableObject ViewModel 中的环境对象。我唯一不喜欢的是视图模型要么在 SceneDelegate 中要么在父视图中公开,我认为这不太正确。对我来说,在视图内部创建视图模型更有意义。但是目前我没有找到解决此问题的方法,并且您的解决方案是迄今为止最好的解决方案。 (4认同)
  • 因此,一方面对于视图,我们可以实现传递依赖项的环境对象样式,另一方面对于 ViewModel,我们需要将其沿链传递(SwiftUI 试图通过引入 EnvironmentObjects 来避免这种情况) (2认同)
  • @Asperi - 这是一个非常好的模式。您是否设法将其调整为与 @StateObjects 一起使用?我收到错误,因为它们似乎是仅获取属性。 (2认同)

Jim*_*lai 10

你不应该。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 {...}
}
Run Code Online (Sandbox Code Playgroud)

我希望人们能体会到 SDK 的设计是多么紧凑。

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

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

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

TL; 博士

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

希望更多的开发人员看到这一点,因为这似乎是一个受欢迎的问题。

  • “ViewModel 是 MVVM 中的模型。” 不。ViewModel 是 MVVM 中的视图模型。模型和视图是其他实体。将 MVVM 与 SwiftUI 结合使用是完全没问题的。 (19认同)
  • 以防万一它对任何人有帮助:这个回复 - 以及我自己对这个主题的一些想法 - 让我暂时远离 MVVM,但我现在正在考虑几个月后带回一些虚拟机(并且意外地再次登陆这里哈哈)。这个回复正确地断言我们在使用 SwiftUI 时有时会太快地跳转到 MVVM,从而增加了不必要的复杂性。然而,当底层问题实际上很复杂时,放弃 MVVM 可能会导致更加复杂的情况。与任何类似的事情一样,教条主义的方法是我唯一可以明确指出并说它是错误的方法。 (9认同)
  • “将 MVVM 与 SwiftUI 结合使用非常好。” 不,它已经过时且多余,即使在 UIKit 中也有更简单的替代方案。我在这里要表达的观点是,您不再需要“视图模型”包装器(无论是否是其自己的实体)来在 SwiftUI 中执行 MVVM。称为“视图模型”的对象从来都不是执行 MVVM 的先决条件。这是关于模型和视图之间的绑定。问问自己,如果您需要为每个视图进行手动视图模型设置,您应该做的第一件事是什么?答案是使其自动化,或者更好的是透明。 (4认同)
  • 写得不错。我正在从 Android 方面解决这个问题,其中 ViewModel(或者至少它是这样命名的)非常常见。发现用 SwiftUI 尝试这种模式非常奇怪(例如,从 Viewmodel 或存储库甚至到 View 的“转发状态绑定”,以便在那里再次绑定)。你的观点非常有道理,我即将去掉 ViewModel,并将 View 对象本身视为一种模型,并将“body: some View”作为 View 部分。好多了。谢谢! (4认同)
  • 那么,在不使用视图模型的情况下,如何使用数据任务发布者通过服务加载数据以在视图中显示? (3认同)
  • “不。ViewModel 是 MVVM 中的视图模型”。这是一个[反例](/sf/ask/774502151/)。 (2认同)
  • 为满足项目需求而定制的专用网络服务对象。将网络与视图或模型解耦。有一种常见的误解,认为必须使用“视图模型”来进行“网络”。不,你使用网络来建立网络。例如; `resource.post(params).onSuccess { json in self.data = json}` 然后使用属性观察器进行模型视图更新,例如;`var data = JSON() { didSet { updateUI() }}`。这些都不需要视图模型的概念,但它做同样的事情。这是没有VM的MVVM,或者说MVVM适应Swift语言特性。 (2认同)
  • 为有兴趣的人提供一个很好的阅读[为什么 mvvm 不好](https://khanlou.com/2015/12/mvvm-is-not-very-good/)。您还应该使用“为什么 mvvm 不好”作为关键字进行搜索,以获得更多见解。请注意,这是 2015 年写的,我向你保证,有了 Swift 和 SwiftUI,MVVM 现在就更没有意义了。您不必为每个视图创建视图模型对象,然后手动为每个视图模型创建绑定。因为它的效率极其低下,而且坦率地说是愚蠢的。 (2认同)
  • 你的意见很有趣@Jimlai,我曾经在Android端应用这个MVVM,并认为我可以在SwiftUI +合并上做同样的事情。既然您建议删除 ViewModel 类,那么整个 iOS 项目结构的最佳实践是什么?例如,对于登录屏幕,如果我们没有 ViewModel,我们应该在哪里调用 Web 服务进行身份验证,然后保存令牌并更新应用程序的状态(isLoggedIn)?我真的不想将所有内容都放在视图文件中,因为那里可能有数千行代码。 (2认同)
  • @Ngyuen Minh Binh 存储库模式解决了关注点分离的问题。完美适用于 SwiftUI,无需 ViewModel。 (2认同)
  • 我看到很多人在 SO 上提出问题,无法完成某些事情,然后显示复杂的代码,这些代码将所有内容混合到一个 SwiftUI 视图中。我们可以做到这一点,甚至像从 UITableViewCell 调用 Core Data 那样笨拙,这是众所周知的事实。但是 MVVM 确实出于某些原因定义了分离和组件。您可以使用 30 行干净漂亮的代码在 SwiftUI 中将 ELM 架构实现到单个视图中,以支持您的想法 - 仍然最好使其可测试、可依赖注入,这需要您接受一些分离的组件。 (2认同)

mca*_*ach 7

你可以这样做:

struct YourView: View {
  @EnvironmentObject var settings: UserSettings

  @ObservedObject var viewModel = YourViewModel()

  var body: some View {
    VStack {
      Text("Hello")
    }
    .onAppear {
      self.viewModel.setup(self.settings)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

对于视图模型:

class YourViewModel: ObservableObject {
  
  var settings: UserSettings?
  
  func setup(_ settings: UserSettings) {  
    self.settings = settings
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 另一个缺点是设置中的更新不会自动传达给视图,因为您将失去 ObservableObject 和 EnvironmentObject 的灵活性。 (9认同)
  • @malhal 我认为 Paul Hudson 不同意你的观点 https://www.hackingwithswift.com/books/ios-swiftui/introducing-mvvm-into-your-swiftui-project (3认同)
  • 这不应该是最佳答案,因为“@ObservedObject var viewModel = YourViewModel()”是错误的。它应该是“@ObservedObject var viewModel:YourViewModel”。并且您使用 ObservedObject 进行外部传递,这意味着视图模型必须初始化并从父视图传递。看看@Asperi 的回答。 (3认同)
  • 缺点是你最终总会有选择。 (2认同)

Mic*_*sky 4

我选择不使用 ViewModel。(也许是时候推出新模式了?)

我已经使用 aRootView和一些子视图设置了我的项目。我将RootView一个App对象设置为环境对象。我的所有视图都访问 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())
   }
}
Run Code Online (Sandbox Code Playgroud)
struct RootView: View {
    @EnvironmentObject var app: App
    
    var body: some View {
        if !app.user.isLoggedIn {
            LoginView()
        } else {
            HomeView()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
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")
          }
       }
    }
}
Run Code Online (Sandbox Code Playgroud)

在我的预览中,我初始化了 a 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
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 该应用程序应该派生自 ObservableObject (2认同)
  • 尽管这种模式一开始很诱人,但一旦 App 发生更改,所有依赖于 App 的视图都会刷新,即使给定的视图没有观察到刚刚更新的特定属性。这是否伤害了您?如果是,您是否找到了减轻这种情况的方法? (2认同)