136

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?

Ralf Ebert
  • 3,556
  • 3
  • 29
  • 43
Michael
  • 1,769
  • 2
  • 12
  • 21
  • Why not pass viewmodel as the EO? – E.Coms Dec 26 '19 at 18:11
  • 1
    Seems over the top, there will be many view models, the upload I have linked is just a simplified example – Michael Dec 26 '19 at 19:17
  • 9
    I'm not sure why this question was downvoted, I'm wondering the same. I'll answer with what I have done, hopefully someone else may come up with something better. – Michael Ozeryansky Dec 26 '19 at 23:38
  • 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. – Michael Ozeryansky Dec 26 '19 at 23:40
  • @Michael Did you even find a solution to this ? – Brett Feb 16 '20 at 22:53
  • 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. – Michael Ozeryansky Feb 17 '20 at 03:56
  • Passing the EnvironmentObject would only be possible in the body. But then the ViewModel keeps getting recreated every time the object is changed. – BobiSad Feb 22 '20 at 21:58
  • No, didn’t find a solution really, just went with a singleton for the session object which allowed me to access it from anywhere – Michael Feb 24 '20 at 08:06

7 Answers7

37

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
  }
}
mcatach
  • 1,227
  • 13
  • 23
  • This is sick.. are there any downsides? No matter how I think.. my swiftui code always end up in MVVM, It's just the most natural and structured! – Kai Zheng Sep 01 '20 at 15:14
  • Didn't see any downsides for now... it works very well, and I'm using it to change Tabs from the viewModel – mcatach Sep 02 '20 at 16:27
  • 8
    The downside is you would always end up having optionals. – G.Abhisek Nov 11 '20 at 11:09
  • 7
    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. – G.Abhisek Nov 12 '20 at 01:08
  • I have observed that the onAppear is called after the view has been displayed. So if you needed to use some logic from the viewModel with the settings you would not get it. – onthemoon Jan 27 '21 at 11:03
  • 1
    This should be the correct and easiest answer! – Björn Sep 22 '21 at 15:53
  • Besides using view model objects in SwiftUI being totally wrong, initing an object with @ObservedObject is a serious error – malhal Mar 10 '22 at 10:55
  • 2
    @malhal I think Paul Hudson doesn't agree with you https://www.hackingwithswift.com/books/ios-swiftui/introducing-mvvm-into-your-swiftui-project – mcatach Mar 10 '22 at 16:56
  • 1
    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. – samwize Apr 24 '23 at 07:28
33

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.

Steffen Funke
  • 2,168
  • 1
  • 21
  • 18
Jim lai
  • 1,224
  • 8
  • 12
  • 1
    Great answer, but what if we need to execute a method in this view which needs the session? Should we define this method in the EO? What if we had 20 methods from diff views ? would you put them all in the EO because of the `state`? – M1X Jun 18 '20 at 12:13
  • 1
    Stateful object in SwiftUI is highlighted by annotation, e.g.; `@State`, `@EnvironmentObject` ... etc. So you could have more than one source of "session" object. Not everything needs to be shared. Also, methods can be easily refactored out to protocol extension. On the other hand, what would MVVM do? Nested / multiple view models? At some point it comes down to your refactor skill. – Jim lai Jul 05 '20 at 04:14
  • 59
    "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. – mcatach Aug 24 '20 at 22:50
  • 4
    "No. ViewModel is a view model in MVVM". Here's a [counter example](https://stackoverflow.com/questions/11064316/what-is-viewmodel-in-mvc). – Jim lai Aug 26 '20 at 13:13
  • It's a common problem that MVVM devs don't really know what a view model is and what it should do. The result is often that it can be anything. A view model being its own entity may have its merits provided dedicated SDK support. But I don't think SwiftUI is designed to support it that way. – Jim lai Aug 26 '20 at 13:22
  • 4
    "It's perfectly fine to use MVVM with SwiftUI." No, it's obsolete and redundant, even in UIKit there are far simpler alternatives. The point I'm making here is that you no longer need a "view model" wrapper (entity of its own or not) to do MVVM in SwiftUI. An object called "view model" has never been a prerequisite to do MVVM. It's about binding between model and view. Ask yourself what is the first thing you should do if you need to do manual view model setup for every view? The answer is to make it automatic, or better yet, transparent. – Jim lai Aug 26 '20 at 13:35
  • I'd also add that if something is called "view model" and it is neither a view nor model, it is a very confusing naming convention. I don't see how it is "perfectly fine" to add all these confusion and ambiguity to SwiftUI. – Jim lai Aug 26 '20 at 15:56
  • 5
    So, without using a view model how would you load data over a service using a data task publisher to display in a view? – Caleb Friden Sep 06 '20 at 05:06
  • 3
    A dedicated network service object customized to fit project needs. Decouple networking from view or model. There's this common misconception that you have to use "view model" to do "networking". No, you use networking to do networking. E.g.; `resource.post(params).onSuccess { json in self.data = json}` then use property observer to do model-view update, e.g.; `var data = JSON() { didSet { updateUI() }}`. None of these require the notion of view model, yet it does the same thing. This is MVVM without VM, or adapting MVVM to Swift language features. – Jim lai Sep 15 '20 at 03:42
  • 2
    Provide a good read [why mvvm is not good](https://khanlou.com/2015/12/mvvm-is-not-very-good/) for those who are interested. You should also search using "why mvvm is not good" as keyword to gain more insights. Note that it is written in 2015, let me assure you that with Swift and SwiftUI, MVVM is even more pointless now. You don't create a view model object for every view then manually create binding for every view model. Because it would be insanely inefficient and frankly dumb. – Jim lai Oct 03 '20 at 17:54
  • 4
    Nice writeup. I am approaching this problem from the Android side, where ViewModel (or at least it is named like so) is very common. Found it very odd trying this pattern with SwiftUI (e.g. "forwarding of state bindings" from Viewmodel, or Repositories even, to View, to bind again there). Your point makes perfect sense, and I am about to strip out the ViewModels, and seeing the View object itself as a kind of model, with the `body: some View` as the View part. Much better. Thanks! – Steffen Funke Feb 17 '21 at 19:27
  • 2
    Your opinion is quite interesting @Jimlai , I used to apply this MVVM at Android side and thought that I can do the same thing at SwiftUI + Combine. Since you suggested to remove ViewModel classes, which is the best practices for entire of iOS project structure? e.g. for Login screen, where should we call web-service for authentication then save token and update app's state (isLoggedIn) if we don't have ViewModel? I don't really want to put everything in the view file since it can be thousand lines of code there. – Nguyen Minh Binh Apr 04 '21 at 09:06
  • 2
    @Ngyuen Minh Binh Repository pattern is what solves the problem of separation of concerns. Perfectly applicable with SwiftUI and no ViewModel needed. – Steffen Funke Apr 17 '21 at 15:48
  • 1
    @Jimlai thinking about it more, I think you're absolutely right. React doesn't use MVVM and instead it takes app state from hooks and transforms to view state in the respective component - I think that's the same idea with SwiftUI. You don't need MVVM because you have EnvironmentObject, and your View is just a struct with a bunch of functions anyway, those functions can transform state (or do whatever they need to do). It's also easily testable by instantiating your view object in XCTest. – SparkyRobinson Jun 01 '21 at 02:59
  • 6
    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. – CouchDeveloper Jul 25 '21 at 22:13
  • 1
    All this is nice chatting but what are the actual solutions? – LetsGoBrandon Aug 18 '21 at 21:20
  • 7
    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. – Alex Fringes Oct 05 '21 at 22:50
  • 1
    I hope the professor from Stanford's SwiftUI course reads this answer soon and stops teaching it. – malhal Mar 10 '22 at 10:54
  • I agree, the EnvironmentObject can do what the viewModel does, so why create something that you already have? – fullmoon Mar 31 '22 at 14:49
  • I also agree with @Jimlai. For very long time, I used MVVM in SwiftUI and I constantly ran into issues. One of the main issues is that we have no access to FocusState, GestureState, EnvironmentObject inside the VM. So we also end up passing it making it more complicated. I have been writing about this a lot you can read it on my blog here: https://azamsharp.com/articles. No one architecture fits all so do your research and find the best solution. – azamsharp Aug 16 '22 at 01:46
  • Valid and invalid point of view at the same time. Let's say you have a VM that doesn't have the Coordinator injected. The Coordinator is EO that is accessible inside the Screen/View. The user taps a button, and you need to make a network call using the VM. And when the call ends you need to call the coordinator. What do you suggest, do you pollute the View with VM call-baks so that you can redirect the actions to the Coordinator? – Laur Stefan Apr 01 '23 at 12:54
  • WAAAAT? )))) Funny morning, thanks lol)) – bodich Jul 06 '23 at 08:59
28

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")
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 3
    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. – Brett Jul 22 '20 at 02:41
  • 2
    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) – G.Abhisek Nov 12 '20 at 00:15
  • In your `SomeView`, should you vm declaration be a `@StateObject` and not an `@ObservedObject`? – SparkyRobinson May 31 '21 at 23:12
  • 1
    @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. – Magnas Jun 27 '21 at 14:08
14

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()
    }
}
emehex
  • 9,874
  • 10
  • 54
  • 100
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
        }
    }

}
LetsGoBrandon
  • 498
  • 8
  • 23
Michael Ozeryansky
  • 7,204
  • 4
  • 49
  • 61
  • 2
    the App should derive from ObservableObject – onthemoon Jan 27 '21 at 11:26
  • You could also use a single init: init(networkManager: NetworkManagerProtocol = NetworkManager()) { – grego Dec 04 '21 at 00:32
  • 2
    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 Dec 23 '21 at 22:45
  • @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. – Michael Ozeryansky Dec 24 '21 at 02:17
  • But how do you reflect "app.userService.logout()" to "app.user.isLoggedIn"? – Martin Majewski Mar 14 '22 at 17:08
  • @MartinMajewski The user object is a published var, the logout function should update the user object. In my app I have a delegate from the service back to App for when the user logs out. The user service could have a publisher for when the user object updates and you could sink that value to the published user object. – Michael Ozeryansky Mar 15 '22 at 05:02
  • @MichaelOzeryansky Thanks. I made it now that way: I pass a closure down to the, e.g., NetworkManager. When it returns (sync or async), it updates the Published var via self-referencing. Your "app" is actually the "ViewModel" with all the business logic for manipulating the At-Published properties. All the binding is done via SwiftUI. I guess seeing an EnvironmentObject as a self-containing data manager, aka. Model-to-View adapter and SwiftUI handling all the data synchronization make the most sense. Maybe we should rename ViewModel to ModelViewAdapter. ;-) – Martin Majewski Mar 15 '22 at 14:38
3

The Resolver library does a nice job to get dependency injection for model classes. It provides a property wrapper @Injected which is very similar in spirit to @EnvironmentObject but works everywhere. So in a model, I would inject a ExampleService like this:

class ExampleModel: ObservableObject {

    @Injected var service: ExampleService

    // ...
}

This can also be used to resolve dependencies for Views:

struct ExampleView: View {

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

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

An alternative for Views is to use @EnvironmentObject in the SwiftUI view hierarchy, but this gets a little bit cumbersome because you'll have two dependency-injection containers, Resolver/@Injected for everything that's app-wide/service-like and SwiftUI/@EnvironmentObject in the view hierarchy for everything that relates to views/for view models.

Ralf Ebert
  • 3,556
  • 3
  • 29
  • 43
  • 2
    I like the concept of `Resolver` and I can see the benefit of having the capability to inject app-wide not only into Views but also into Models. However, on second thought I don't like to depend on a 3rd party solution. Isn't there a nice SwiftUI-Combine-only way ? – iKK Apr 15 '21 at 16:01
0

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

fatihyildizhan
  • 8,614
  • 7
  • 64
  • 88
ThisIsMe
  • 58
  • 5
  • 5
    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. – Michele Giuseppe Fadda Jan 20 '23 at 14:44
  • 1
    It can easily become an antipattern if you have multiple environment objects that need to bed injected like various network services, storage, and coordinators. – Laur Stefan Apr 01 '23 at 12:21