Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - How to pass EnvironmentObject into View Model?

I'm looking to create an EnvironmentObject that can be accessed by the View Model (not just the view).

The Environment object tracks the application session data, e.g. loggedIn, access token etc, this data will be passed into the view models (or service classes where needed) to allow calling of an API to pass data from this EnvironmentObjects.

I have tried to pass in the session object to the initialiser of the view model class from the view but get an error.

how can I access/pass the EnvironmentObject into the view model using SwiftUI?

like image 867
Michael Avatar asked Dec 26 '19 17:12

Michael


3 Answers

Below provided approach that works for me. Tested with many solutions started with Xcode 11.1.

The problem originated from the way EnvironmentObject is injected in view, general schema

SomeView().environmentObject(SomeEO())

ie, at first - created view, at second created environment object, at third environment object injected into view

Thus if I need to create/setup view model in view constructor the environment object is not present there yet.

Solution: break everything apart and use explicit dependency injection

Here is how it looks in code (generic schema)

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

There is no any trade-off here, because ViewModel and EnvironmentObject are, by design, reference-types (actually, ObservableObject), so I pass here and there only references (aka pointers).

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")
    }
}
like image 145
Asperi Avatar answered Nov 02 '22 17:11

Asperi


You can do it like this:

struct YourView: View {
  @EnvironmentObject var settings: UserSettings

  @ObservedObject var viewModel = YourViewModel()

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

For the ViewModel:

class YourViewModel: ObservableObject {
  
  var settings: UserSettings?
  
  func setup(_ settings: UserSettings) {  
    self.settings = settings
  }
}
like image 36
mcatach Avatar answered Nov 02 '22 19:11

mcatach


You shouldn't. It's a common misconception that SwiftUI works best with MVVM. MVVM has no place in SwiftUI. You are asking that if you can shove a rectangle to fit a triangle shape. It wouldn't fit.

Let's start with some facts and work step by step:

  1. ViewModel is a model in MVVM.

  2. MVVM does not take value types (e.g.; no such thing in Java) into consideration.

  3. A value type model (model without state) is considered safer than reference type model (model with state) in the sense of immutability.

Now, MVVM requires you to set up a model in such way that whenever it changes, it updates the view in some pre-determined way. This is known as binding.

Without binding, you won't have nice separation of concerns, e.g.; refactoring out model and associated states and keeping them separate from view.

These are the two things most iOS MVVM developers fail:

  1. iOS has no "binding" mechanism in traditional Java sense. Some would just ignore binding, and think calling an object ViewModel automagically solves everything; some would introduce KVO-based Rx, and complicate everything when MVVM is supposed to make things simpler.

  2. Model with state is just too dangerous because MVVM put too much emphasis on ViewModel, too little on state management and general disciplines in managing control; most of the developers end up thinking a model with state that is used to update view is reusable and testable. This is why Swift introduces value type in the first place; a model without state.

Now to your question: you ask if your ViewModel can have access to EnvironmentObject (EO)?

You shouldn't. Because in SwiftUI a model that conforms to View automatically has reference to EO. E.g.;

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

I hope people can appreciate how compact SDK is designed.

In SwiftUI, MVVM is automatic. There's no need for a separate ViewModel object that manually binds to view which requires an EO reference passed to it.

The above code is MVVM. E.g.; a model with binding to view. But because model is value type, so instead of refactoring out model and state as view model, you refactor out control (in protocol extension, for example).

This is official SDK adapting design pattern to language feature, rather than just enforcing it. Substance over form. Look at your solution, you have to use singleton which is basically global. You should know how dangerous it is to access global anywhere without protection of immutability, which you don't have because you have to use reference type model!

TL;DR

You don't do MVVM in java way in SwiftUI. And the Swift-y way to do it is no need to do it, it's already built-in.

Hope more developer see this since this seemed like a popular question.

like image 22
Jim lai Avatar answered Nov 02 '22 19:11

Jim lai