#4 – Accessing environment variables in the view model

One of the most powerful features of SwiftUI is the environment. Environment variables propagate downward, meaning that all subviews can access them as needed. What’s wonderful about this is that we don’t need to include them in our view initializers—they just work. There is a catch, though—only SwiftUI views can access environment variables.

In the coordinator pattern, dependency injection is typically handled by the coordinator. Traditionally, coordinators are not views, but fortunately, our implementation of the coordinator pattern is done in the context of a view. This approach significantly benefits this version of the MVVM-C pattern in SwiftUI.

In our setup, we can set environment variables at the root level of the app, access them at the coordinator level as needed, and ultimately inject them into our view models.

A huge benefit of doing this is that it almost completely eliminates boilerplate code related to dependency injection.

Say I need access to my instance of CoreData’s NSManagedObjectContext in a view that is 8 levels deep in my navigation stack. I could always wrap it in a singleton and just access it anytime I need it with one line of code. But singletons are generally frowned upon for several reasons that I won’t go into here.

Since I am generally against the use of singletons, you might assume that I’d have to add boilerplate to inject my object into all eight coordinators. Even for coordinators where it is completely unused, I have to inject it just to act as a bridge to get it to its final destination. This is less than ideal and leads to huge initializers and messy code.

But thanks to the SwiftUI environment, the only coordinator that would need to know about my object is the one responsible for injecting it into its view model.

Let’s look at some code.

First, we’ll set up a CoreData wrapper called PersistenceManager. We won’t give it any functionality quite yet, but we need something to work with, so let’s put together something very basic:

import CoreData
import Observation

@Observable
class PersistenceManager {
    let persistentContainer: NSPersistentContainer

    init() {
        persistentContainer = PersistenceManager.createContainer()
    }
}

private extension PersistenceManager {
    static func createContainer() -> NSPersistentContainer {
        let container = NSPersistentContainer(name: "Data")
        container.loadPersistentStores { (storeDescription, error) in
            if let error {
                fatalError(error.localizedDescription)
            }
        }
        return container
    }
}

Next, I’ll create a data model simply called Data and place it in my project. I’ll leave it empty for now. Again, we just need something to work with.

Now, at the root level of my app, I’ll create my instance of PersistenceManager and inject it into the environment:

import SwiftUI

@main
struct BKSwiftyApp: App {
    let persistenceManager = PersistenceManager()
    
    var body: some Scene {
        WindowGroup {
            PeopleCoordinator()
                .environment(persistenceManager)
        }
    }
}

Notice how I marked PersistenceManager with the @Observable attribute. While this isn’t strictly necessary, it makes injecting it into the environment seamless. If I didn’t want to (or couldn’t) mark it with @Observable, I could still make it an environment variable with the extra step of creating a custom EnvironmentKey.

So, now that PersistenceManager exists in my environment, I need to access it in one of my coordinators.

Let’s access it in the PeopleCoordinator and inject it into the PeopleViewModel:

struct PeopleCoordinator: View {
    @Environment(PersistenceManager.self)
    var persistenceManager
    
    var body: some View {
        NavigationStack {
            PeopleView(viewModel: createViewModel())
        }
    }
    
    func createViewModel() -> PeopleView.ViewModel {
        .init(persistenceManager: persistenceManager)
    }
}
extension PeopleView {
    @Observable
    class ViewModel {
        var persistenceManager: PersistenceManager
        var people: [String]
        
        init(persistenceManager: PersistenceManager,
             people: [String] = ["Ben", "Jessica", "Nora"]) {
            self.persistenceManager = persistenceManager
            self.people = people
        }
    }
}

And that’s how to use the SwiftUI environment for dependency injection.

In my next post, we’ll expand on that CoreData implementation and build it out a bit more.

Updated code:

https://github.com/benmakesapps/BKSwifty/tree/4


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *