SwiftUI -如何将环境对象传递到视图模型中?

cgfeq70w  于 2022-12-21  发布在  Swift
关注(0)|答案(9)|浏览(257)

我希望创建一个可以被视图模型(不仅仅是视图)访问的EnvironmentObject。
环境对象跟踪应用程序会话数据,例如登录、访问令牌等,该数据将被传递到视图模型(或服务类,如果需要),以允许调用API来传递来自该环境对象的数据。
我试图将会话对象从视图传递给视图模型类的初始化程序,但是得到了一个错误。
如何使用SwiftUI访问/传递EnvironmentObject到视图模型中?

wwtsj6pe

wwtsj6pe1#

下面提供了适合我的方法。用许多从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")
    }
}
7cwmlq89

7cwmlq892#

你可以这样做:

struct YourView: View {
  @EnvironmentObject var settings: UserSettings

  @ObservedObject var viewModel = YourViewModel()

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

对于视图模型:

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

up9lanfz3#

你不应该这样做。SwiftUI与MVVM配合使用效果最好是一个常见的误解。MVVM在SwiftUI中没有位置。你是在问你是否可以将一个矩形推到一个三角形中。它不适合。
让我们从一些事实开始,一步一步来:

  1. ViewModel是MVVM中的一个模型。
  2. MVVM不接受值类型(例如; Java中没有这样的东西)。
    1.从不变性的意义上讲,值类型模型(没有状态的模型)被认为比引用类型模型(有状态的模型)更安全。
    现在,MVVM要求您以这样的方式设置模型,即无论它何时改变,它都以某种预定义的方式更新视图,这称为绑定。
    没有绑定,就不会有很好的关注点分离,例如:重构出模型和相关状态,并将它们与视图分开。
    这是大多数iOS MVVM开发人员都会失败的两件事:
  3. iOS没有传统Java意义上的"绑定"机制,有些人会忽略绑定,认为调用一个对象ViewModel就可以自动解决一切问题;有些人会引入基于KVO的Rx,并在MVVM应该使事情更简单时使一切复杂化。
    1.带状态的模型太危险了,因为MVVM太强调视图模型,而对状态管理和管理控制的一般规则太少;大多数开发者最终认为一个带有状态的模型是可以重用和测试的,这就是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,例如:但是因为模型是值类型,所以不是重构出模型和状态作为视图模型,而是重构出控制(例如在协议扩展中)。
这是官方的SDK,它使设计模式适应语言特性,而不仅仅是强制它。实质重于形式。看看你的解决方案,你必须使用基本上是全局的单例。你应该知道在没有不变性保护的情况下访问全局是多么危险,你没有不变性保护,因为你必须使用引用类型模型!

    • TL; DR**

你不用在SwiftUI中用java的方式来做MVVM,而且Swift-y的方式也不需要做,它已经内置了。
希望更多的开发人员看到这一点,因为这似乎是一个流行的问题。

wz3gfoph

wz3gfoph4#

解决方案:iOS 14/15以上版本
以下是您可以如何从视图模型与环境对象交互,而不必在示例化时注入它:
1.定义环境对象:

import Combine

final class MyAuthService: ObservableObject {
    @Published private(set) var isSignedIn = false
    
    func signIn() {
        isSignedIn = true
    }
}

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()
        }
    }
}

1.使用将环境对象作为参数的方法定义视图模型:

extension MyAuthView {
    @MainActor final class ViewModel: ObservableObject {
        func signIn(with auth: MyAuthService) {
            auth.signIn()
        }
    }
}

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")
        }
    }
}

1.预览以确保完整性:

struct MyEntryPointView_Previews: PreviewProvider {
    static var previews: some View {
        MyEntryPointView()
    }
}
gk7wooem

gk7wooem5#

我选择不使用ViewModel。(也许是时候使用新模式了?)
我已经用RootView和一些子视图设置了我的项目。我用App对象作为EnvironmentObject设置了我的RootView。我的所有视图都访问应用程序上的类,而不是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
        }
    }

}
yyyllmsg

yyyllmsg6#

Resolver库很好地为模型类进行了依赖注入,它提供了一个属性 Package 器@Injected,它在本质上与@EnvironmentObject非常相似,但在任何地方都可以工作,因此在模型中,我将注入一个ExampleService,如下所示:

class ExampleModel: ObservableObject {

    @Injected var service: ExampleService

    // ...
}

这也可用于解析视图的相关性:

struct ExampleView: View {

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

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

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

zu0ti5jz

zu0ti5jz7#

只需创建一个Singleton并在任何需要的地方使用它(视图/类/结构/ ObservableObject...)

创建类应如下所示:

class ApplicationSessionData
{
    // this is the shared instance / local copy / singleton
    static let singleInstance = ApplicationSessionData()

    // save shared mambers/vars here
    var loggedIn: Bool = false
    var access: someAccessClass = someAccessClass()
    var token: String = "NO TOKET OBTAINED YET"
    ...
}

使用类/结构/视图应如下所示:

struct SomeModel {
    // obtain the shared instance
    var appSessData = ApplicationSessionData.singleInstance

    // use shared mambers/vars here
    if(appSessData.loggedIn && appSessData.access.hasAccessToThisView) {
        appSessData.token = "ABC123RTY..."
    ...
    }
}

你需要意识到单身者存在的陷阱,这样你就不会陷入其中,
阅读更多:https://matteomanferdini.com/swift-singleton

yruzcnhs

yruzcnhs8#

这是我发现的访问和更新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的方法不同,它没有可选性,可以在onAppear被触发之前运行代码,视图模型的范围仅限于它所服务的视图。
您还可以在viewModel中直接修改store,这也可以正常工作。

h79rfbju

h79rfbju9#

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

// 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
    }
}

相关问题