Custom Environment Values in SwiftUI

In SwiftUI we have a few different ways to pass data from parent views to children, such as passing values and objects in initializers, using environment objects or using environment key-value pairs.

Passing values in initializes of views can get repetitive and can clutter your code especially if the views that need them are deeply nested in the hierarchy. That's why environment objects and environment values are both great for it. You can set them on the parent view and any of the direct or nested children can access the data using @EnvironmentObject or @Environment property wrappers respectively.

We use environment objects when multiple views need to be able to modify view state or model data and get updated when data changes. But for passing UI specific values that only influence the layout of the views, we prefer to use environment variables.

Define environment key-value pairs

It's commonly believed that @Environment only works with keys pre-defined by the framework. But it's also possible to define our own custom keys and use them in our views.

As an example we will create a parent view that is inside a GeometryReader and pass the view size to its children. Then any of the child views can read the size value to layout itself accordingly. Here is the initial setup.

struct ContentView: View {
    var body: some View {
        GeometryReader { geo in
            VStack(spacing: 16) {
                AppDescription()
                SubscriptionButtonsStack()
            }            
        }
    }
}

We need to define our custom key conforming to EnvironmentKey protocol and give it a default value.

struct ParentSizeEnvironmentKey: EnvironmentKey {
    static let defaultValue: CGSize? = nil
}

Then we will extend EnvironmentValues with our computed property.

extension EnvironmentValues {
    var parentSize: CGSize? {
        get {
            return self[ParentSizeEnvironmentKey.self]
        }
        set {
            self[ParentSizeEnvironmentKey.self] = newValue
        }
    }
}

And now we can set a value for this new custom key in our view.

GeometryReader { geo in
    VStack(spacing: 16) {
        AppDescription()
        SubscriptionButtonsStack()
    }
    .environment(\.parentSize, geo.size)
}

Read environment values

Any of the child views (direct or nested) can now read the size value from the environment.

In our example AppDescription view will only show the longest paragraph if the view is tall enough.

struct AppDescription: View {
    @Environment(\.parentSize) var parentSize
    
    var hasSmallHeight: Bool {
        if let parentSize = parentSize,
            parentSize.height <= SizeConstants.smallHeight {
            return true
        }
        return false
    }
    
    var body: some View {
        VStack(spacing: 16) {
            Text("App Title")
                .font(.title)
            Text("This is a really great app.")
                .font(.headline)
            
            if !hasSmallHeight {
                Text("""
                    This app lets you do lots of amazing things.
                    You should definitely consider subscribing now.
                """)
            }
        }
    }
}

We are using @Environment property wrapper to read the size value that we set in the parent view from the environment.

Define a custom function for setting the value

To make the code for setting our environment value nicer, it's possible to define a function on View protocol for our custom key.

extension View {
    func parentSize(_ size: CGSize?) -> some View {
        return self.environment(\.parentSize, size)
    }
}

Then we can just call this function on our view and pass in the size value.

GeometryReader { geo in
    VStack(spacing: 16) {
        AppDescription()
        SubscriptionButtonsStack()
    }
    .parentSize(geo.size)    
}

You can find the complete code for the example described here in GitHub folder for this article.