172

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?

5
  • Why not pass viewmodel as the EO? Commented Dec 26, 2019 at 18:11
  • 1
    Seems over the top, there will be many view models, the upload I have linked is just a simplified example Commented Dec 26, 2019 at 19:17
  • 2
    @E.Coms I expected EnvironmentObject to generally be one object. I know multiple work, it seems like a code smell to make them globally accessible like that. Commented Dec 26, 2019 at 23:40
  • Other SOs mentioned passing the EnvironmentObject in the ViewModel, but that looked terrible to me. I personally choose no ViewModels, have objects on the EnvironmentObject, and access those directly. Like Button(action: { app.widgetController.doAction() }).... Other examples have ViewModels which access shared instances and don't have a need for EnvironmentObject, but I prefer pure dependency injection over shared instances. Commented Feb 17, 2020 at 3:56
  • Passing the EnvironmentObject would only be possible in the body. But then the ViewModel keeps getting recreated every time the object is changed. Commented Feb 22, 2020 at 21:58

7 Answers 7

43

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
  }
}
Sign up to request clarification or add additional context in comments.

7 Comments

The downside is you would always end up having optionals.
One more downside is your updates in settings will not be communicated to view automatically as you would lose the flexibility of ObservableObject and EnvironmentObject.
@malhal I think Paul Hudson doesn't agree with you hackingwithswift.com/books/ios-swiftui/…
This shouldn't be the top answer because it is wrong to @ObservedObject var viewModel = YourViewModel(). It should be @ObservedObject var viewModel: YourViewModel. And you use ObservedObject to pass externally, that means the view model has to be initialized and passed from the parent view. Look at the answer by @Asperi.
This answer is not correct because if you are initializing ViewModel inside the View then you should use @StateObject instead, so it should be: @StateObject var viewModel = YourViewModel() If you use @ObservedObject this way, then view model can be initialized many times again and again (for example it might be initialized again if you move to another screen in navigationstack and then go back to this one) which is probably not what you want.
|
32

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

4 Comments

I am just starting out with MVVM and this is the closest thing to what I want to do. I was surprised that I couldn't access my EnvironmentObjects inside my ObservableObject ViewModel. The only thing I don't like is that the view model is exposed either in the SceneDelegate or in the parent view, which I don't think is quite right. It makes more sense to me for the View Model to be created inside the View. However currently I don't see a way around this and your solution is the best so far.
So on one hand for views, we can implement the environment object styling of passing dependencies on the other hand for ViewModels, we need tp pass it down the chain (which SwiftUI tries to avoid by introducing EnvironmentObjects)
In your SomeView, should you vm declaration be a @StateObject and not an @ObservedObject?
@Asperi - This is a very nice pattern. Have you managed to adapt it for use with @StateObjects? I’m getting an error because they seem to be a get-only property.
27

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.

18 Comments

"ViewModel is a model in MVVM." No. ViewModel is a view model in MVVM. The model and the view are other entities. It's perfectly fine to use MVVM with SwiftUI.
"No. ViewModel is a view model in MVVM". Here's a counter example.
So, without using a view model how would you load data over a service using a data task publisher to display in a view?
I see a lot of people asking here on SO having trouble to get something done and then showing convoluted code which mixes everything together into a single SwiftUI view. That we can do this, and even awkward suff like calling Core Data from a UITableViewCell, is a well known fact. But MVVM does define separation and components for reasons. You can implement an ELM architecture in SwiftUI into a single View in 30 lines of clean and nice code which supports your idea - still it's better to make it testable, dependency injectable and this requires you to accept some separated components.
Just in case it helps anyone: this reply — and some of my own thoughts on the topic — put me on a path away from MVVM for a while but I am now considering bringing some VMs back months later (and landed here again by accident ha). This reply is correct in asserting that we sometimes jump to MVVM too quickly when working with SwiftUI, thereby adding unnecessary complexity. However, when the underlying problem is actually complex, dropping MVVM can be a source of even more complexity. As with anything like this, a dogmatic approach is the only one I can clearly point to and say that it is wrong.
|
19

Solution for: iOS 14/15+

Here's how you might interact with an Environment Object from a View Model, without having to inject it on instantiation:

  1. Define the Environment Object:
import Combine

final class MyAuthService: ObservableObject {
    @Published private(set) var isSignedIn = false
    
    func signIn() {
        isSignedIn = true
    }
}
  1. Create a View to own and pass around the Environment Object:
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. Define the View Model with methods that take the Environment Object as an argument:
extension MyAuthView {
    @MainActor final class ViewModel: ObservableObject {
        func signIn(with auth: MyAuthService) {
            auth.signIn()
        }
    }
}
  1. Create a View that owns the View Model, receives the Environment Object, and calls the appropriate method:
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. Preview it for completeness:
struct MyEntryPointView_Previews: PreviewProvider {
    static var previews: some View {
        MyEntryPointView()
    }
}

2 Comments

Ideal in a small example, but if your model has a lot of different methods/getters/dependencies, this adds a lot of boilerplate to each call
@VolodymyrBobyr why would this create boilerplate? Compared to the examples which maintain a reference to the EnvironmentObject (EO) in the view model, there is far less boilerplate with this aproach - no extra inits and setup required, the EO just gets passed as a parameter.
3

I choose to not have a ViewModel. (Maybe time for a new pattern?)

I have setup my project with a RootView and some child views. I setup my RootView with a App object as the EnvironmentObject. Instead of the ViewModel accessing Models, all my views access classes on App. Instead of the ViewModel determining the layout, the view hierarchy determine the layout. From doing this in practice for a few apps, I've found my views are staying small and specific. As an over simplification:

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

In my previews, I initialize a MockApp which is a subclass of App. The MockApp initializes the designated initializers with the Mocked object. Here the UserService doesn't need to be mocked, but the datasource (i.e. NetworkManagerProtocol) does.

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

}

9 Comments

the App should derive from ObservableObject
You could also use a single init: init(networkManager: NetworkManagerProtocol = NetworkManager()) {
Although this pattern is tempting at first, all views that depend on App would be refreshed as soon as App changes, even if given views aren't observing the specific property that just got updated. Did this hurt you, and if so, did you find a way to mitigate this?
@pommefrite I never had that issue, and I've profiled all my apps since there are a lot of inefficiencies within SwiftUI only seen from Instruments. I can't see it ever being an issue since animations aren't done with EnvironmentObjects and the view function just return a struct which Apple optimized specifically for SwiftUI.
But how do you reflect "app.userService.logout()" to "app.user.isLoggedIn"?
|
-1

Access the environment object in the parent view and pass it to the subview where the ViewModel is initialized. The parent view should handle injecting the environment object into the subview, ensuring the ViewModel has what it needs during initialization.

Comments

-3

Simply create a Singleton and use it wherever you want (view / class / struct / ObservableObject ...)

Creating Class should look like this:

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

Using Class/Struct/View should look like this:

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

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

You need to be aware of the pitfalls that exist in Singletons, so you won't fall into one.

Read more here: https://matteomanferdini.com/swift-singleton

3 Comments

It is so hard not to fall into the pitfalls of Singleton that Singleton is generally discouraged and often considered an antipattern as it shares global state. I really don't think this approach has merit.
It can easily become an antipattern if you have multiple environment objects that need to bed injected like various network services, storage, and coordinators.
Singletons can be fine if you have a modicum of technical design chops. And in cases where you're relying on the environment to make things available in the views to hand off, it's a good way to initialize things. By all means be careful, but just keep your head about you and singletons are fine... or fine enough. Let's stop trashing thing without some serious discussion on why they're being trashed. It's mostly a religious discussion when people don't explain the "here's why it's bad" details to someone just learning about them.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.