我正在寻找创建一个可由视图模型(而不仅仅是视图)访问的EnvironmentObject。
Environment对象跟踪应用程序会话数据,例如登录,访问令牌等,这些数据将传递到视图模型(或需要的服务类)中,以允许调用API来传递来自此EnvironmentObjects的数据。
我试图将会话对象从视图传递给视图模型类的初始化程序,但会收到错误消息。
如何使用SwiftUI将EnvironmentObject访问/传递到视图模型?
请参阅测试项目的链接:https://gofile.io/?c=vgHLVx
答案 0 :(得分:5)
下面提供了对我有用的方法。经过了许多从Xcode 11.1开始的解决方案的测试。
问题源于在普通模式中将EnvironmentObject注入视图的方式
Example:
From that:
[UID] [Name]
54 Name1
24 Name5
8 Name6
50 Name7
19 Name10
to like that: (randomly)
[UID] [Name]
50 Name7
8 Name6
19 Name10
54 Name1
24 Name5
即,第一个创建的视图,第二个创建的环境对象,注入到视图中的第三个环境对象
因此,如果需要在视图构造函数中创建/设置视图模型,则环境对象尚不存在。
解决方案:分解一切并使用显式依赖项注入
这是它在代码(通用模式)中的外观
SomeView().environmentObject(SomeEO())
这里没有任何取舍,因为ViewModel和EnvironmentObject在设计上是引用类型(实际上是// 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)
),所以我在这里只传递了引用(也就是指针)。
ObservableObject
答案 1 :(得分:5)
不应该。常见的误解是,SwiftUI与MVVM配合使用效果最佳。
MVVM在SwfitUI中没有位置。您在问是否可以将矩形推到
适合三角形。那不合适。
让我们从一些事实开始并逐步进行工作:
ViewModel是MVVM中的模型。
MVVM不考虑值类型(例如,java中没有此类内容)。
值类型模型(无状态模型)被认为比参考安全
不变性意义上的型模型(具有状态的模型)。
现在,MVVM要求您以这样一种方式建立模型,即只要模型发生更改,它便会
以某种预定的方式更新视图。这称为绑定。
没有约束,您将无法很好地分离关注点,例如;重构
模型和关联状态,并使它们与视图分离。
这是大多数iOS MVVM开发人员失败的两件事:
iOS在传统的Java意义上没有“绑定”机制。
有些人只是忽略绑定,而认为调用对象ViewModel
自动解决所有问题;有些人会引入基于KVO的Rx,以及
当MVVM使事情变得更简单时,会使一切变得复杂。
具有状态的模型太危险了
因为MVVM过分强调ViewModel,而过分强调状态管理
和控制管理的一般学科;大多数开发人员最终
使用状态来更新视图的模型是 reuseable 和
可测试。
这就是为什么Swift首先引入值类型的原因;没有
的模型状态。
现在是您的问题:您问ViewModel是否可以访问EnvironmentObject(EO)?
不应该。因为在SwiftUI中,符合View的模型会自动具有
对EO的引用。例如;
struct Model: View {
@EnvironmentObject state: State
// automatic binding in body
var body: some View {...}
}
我希望人们能够欣赏紧凑型SDK的设计方式。
在SwiftUI中,MVVM是自动。不需要单独的ViewModel对象
手动绑定到需要传递EO参考的视图。
上面的代码是 MVVM。例如。;具有绑定视图的模型。
但是因为模型是值类型,所以与其将模型和状态重构为
视图模型,您可以重构控制权(例如,在协议扩展中)。
这是适用于语言功能的官方设计工具包,不仅仅是
强制执行。实质重于形式。
看看您的解决方案,您必须使用基本上是全局的单例。你
应该知道在没有
保护的情况下在任何地方访问全局有多危险不变性,因为您必须使用引用类型模型,所以它没有!
TL; DR
您不会在SwiftUI中以Java方式执行MVVM。不需要Swift-y的方法
要做到这一点,它已经内置。
希望更多的开发人员看到这一点,因为这似乎是一个受欢迎的问题。
答案 2 :(得分:3)
您可以这样做:
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
}
}
答案 3 :(得分:1)
Resolver库很好地完成了对模型类的依赖注入。它提供了一个属性包装器@Injected
,其本质与@EnvironmentObject
相似,但可以在任何地方使用。因此,在模型中,我将像这样注入XYZService:
class SomeModel: ObservableObject {
@Injected var service: XYZService
}
我会在SwiftUI视图层次结构中将这种模型用作@EnvironmentObject。
这有点涉及,因为您将有两个依赖项注入容器,即Resolver / @ Injected用于与应用程序范围/服务类似的所有内容,而SwiftUI / @ EnvironmentObject用于视图层次结构中与视图相关的所有内容查看模型。但是,在更复杂的应用程序中,它非常适合并且值得增加其他复杂性。比singletons / XYZService.shared更好...
答案 4 :(得分:0)
这是我发现的在 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)
}
}
}
这种方法还允许您在单元测试 store
或在画布预览中通过依赖项注入模拟 ChildViewModel
。
与其他使用 onAppear
的 hacky 方法不同,没有可选项,可以在触发 onAppear 之前运行代码,并且视图模型的范围仅限于它所服务的视图。
您也可以直接修改 viewModel 中的 store
,这也很好用。