@EnvironmentObject explained for sharing data between views in SwiftUI

@EnvironmentObject is part of the family of SwiftUI Property Wrappers that can make working with SwiftUI views a little easier. Sharing data between views can be challenging when working in SwiftUI, especially when we have a lot of child views being dependent on the same piece of data.

We could solve dependency injection by inserting the relevant value through each initializer. However, this would require us to pass the dependency into each view manually. Doing so might be sufficient for views one level deeper, but once we have multiple child views, this can quickly become boilerplate code. Environment objects answer this and make it even possible to provide values a few levels deep through child views that don’t use the value itself.

What is an @EnvironmentObject?

An @EnvironmentObject is an object living in the current environment, available to read whenever needed. An environment object is defined at a higher-level view, and can any child view can access it if needed.

For example, we could define an app theme inside our app struct:

import SwiftUI

final class Theme: ObservableObject {
    @Published var primaryColor: Color = .orange
}

@main
struct EnvironmentObjectsExampleApp: App {
    @StateObject var currentTheme = Theme()

    var body: some Scene {
        WindowGroup {
            ArticlesListView()
                .environmentObject(currentTheme) // Make the theme available through the environment.
        }
    }
}

The theme currently represents a single primary color value that you could potentially change within the app with a theme selector. We are required to mark the theme as observable by inheriting from ObservableObject since we would otherwise run into the following error:

Instance method ‘environmentObject’ requires that ‘Theme’ conform to ‘ObservableObject’

Defining our theme as observable is excellent since it allows us to update any dependent views once our primary color changes automatically.

We are making use of the environmentObject view modifier to provide the environment object into the ArticlesListView. After delivering the object, we can make use of it by defining the environment object as follows:

struct ArticlesListView: View {

    @EnvironmentObject var theme: Theme

    // .. view definition

}

We don’t have to provide a default value since the environment gives it. We can access any of the theme colors after making the theme available:

Text(article.title)
    .foregroundColor(theme.primaryColor)

The foreground color of our article title label will be updated automatically when the primary color changes since the theme is an observed object.

How does SwiftUI know which environment value to use?

Environment objects are matched based on the object type. When reading an environment object of type Theme, SwiftUI tries to find an input environment object of the same type. Since matching is type-based, you have to be careful when defining multiple environment objects of the same type.

Using multiple environment objects

You can define as many environment objects as you like. The principle works precisely the same, as long as you provide an environment object through the view modifier for each requested environment object type.

However, as mentioned before, you have to be careful when defining multiple environment objects of the same type:

@main
struct EnvironmentObjectsExampleApp: App {

    /// Both red and green have the same type `Theme`.
    @StateObject var redTheme = Theme(primaryColor: .red)
    @StateObject var greenTheme = Theme(primaryColor: .green)

    var body: some Scene {
        WindowGroup {
            ArticlesListView()
                .environmentObject(redTheme) // The first object takes priority over any following objects.
                .environmentObject(greenTheme)
        }
    }
}

The first provided environment object will always take precedence over any following defined environment objects on the same view. However, if we define the red theme environment object on an outer container, the green theme would take precedence:

var body: some Scene {
    WindowGroup {
        VStack {
            ArticlesListView()
                // The object closer to the view takes priority over any parent objects.
                .environmentObject(greenTheme)
        }.environmentObject(redTheme)
    }
}

Understanding priority when working with multiple environment objects is essential to not end up debugging for a long time. A trick could be to define a color on your environment object and apply that to your view just for debugging. The color can help you indicate the active environment object.

What if I forget to configure the environment object?

A common issue when working with environment objects is forgetting to provide the input value into the environment. Though, you’ll know soon enough when you forgot to do so since your app will throw an exception:

"The environment object may be missing as an ancestor of this view" as a thrown error
“The environment object may be missing as an ancestor of this view” as a thrown error

The error indicates a missing environment object provider on any ancestors of the current view. Whenever you run into this error, you’ll have to go up to the parent view and verify whether you correctly provided the environment object.

Do I need to forward environment objects?

There’s no need to forward any environment objects if you only want to use the value a few levels deep. For example, you could define an environment object in view A and only read from it in view D:

  • View A – defines the environment object
    • View B
      • View C
        • View D – reads the environment object

View B and C are in between but don’t do anything with the environment object. View D can still read the environment object since one of its parents (View A) defined the value.

Why wouldn’t I use Dependency Injection instead?

When I started looking into @EnvironmentObject usage, I asked myself: why wouldn’t I use dependency injection instead? It would require me to write as much code since I must pass the environment value through the initializer vs. defining it through a view modifier.

The most significant advantage of using an environment object is making it available to any of the child views if needed. The previous paragraph demonstrated that we don’t have to pass an environment object manually to any children. If we were to use dependency injection, we would have had to pass and define the value within each child:

  • View A – defines the object to pass it into B
    • View B – defines the object to pass it into C
      • View C – defines the object to pass it into D
        • View D – reads the injected object

You can see that it’s a lot of redundant code when the object is not used within views B and C. Therefore, it can be beneficial to use an environment object instead and make it available to any child views if needed.

Conclusion

You can use the @EnvironmentObject Property Wrapper in SwiftUI to provide environment values to child views without injecting them manually. You can read environment values a few levels deep, even though in-between views don’t do anything with it. You have to be careful when working with multiple environment values of the same type since there’s a priority system in place.

If you like to improve your SwiftUI knowledge even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

 

Featured SwiftLee Jobs

Loading RSS Feed

Browse more Swift related Jobs, or add your own on SwiftLee Jobs