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

时间:2019-12-26 17:42:40

标签: ios swift mvvm swiftui

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

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

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

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

请参阅测试项目的链接:https://gofile.io/?c=vgHLVx

5 个答案:

答案 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中没有位置。您在问是否可以将矩形推到

适合三角形。那不合适。

让我们从一些事实开始并逐步进行工作:

  1. ViewModel是MVVM中的模型。

  2. MVVM不考虑值类型(例如,java中没有此类内容)。

  3. 值类型模型(无状态模型)被认为比参考安全

    不变性意义上的

    型模型(具有状态的模型)。

现在,MVVM要求您以这样一种方式建立模型,即只要模型发生更改,它便会

以某种预定的方式更新视图。这称为绑定。

没有约束,您将无法很好地分离关注点,例如;重构

模型和关联状态,并使它们与视图分离。

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

  1. iOS在传统的Java意义上没有“绑定”机制。

    有些人只是忽略绑定,而认为调用对象ViewModel

    自动解决所有问题;有些人会引入基于KVO的Rx,以及

    当MVVM使事情变得更简单时,会使一切变得复杂。

  2. 具有状态的模型太危险了

    因为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,这也很好用。