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.



Defining 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]
        }
        set {
            self[ParentSizeEnvironmentKey] = 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)
}



Using Environment Values


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


In our example the 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.


Tips


To make the code for setting our environment value nicer, it's possible to define a function on the 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)    
}



It's important to remember that the environment doesn't get inherited by sheets and popovers. So you will need to capture the values and pass them through. You can read more about it in this article.


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