on
SwiftUI - Creating a custom @Environment
SwiftUI enables us to access common pieces of information throughout the app using @Environment
objects avoiding the cumbersomeness of passing data through the views.
There are many built-in environments such as openURL
, colorScheme
or managedObjectContext
that are defined in the Swift standard library as part of an extension of the EnvironmentValues
.
extension EnvironmentValues {
public var managedObjectContext: NSManagedObjectContext
}
While these built-in objects are very convenient, SwiftUI gives us the ability to create tailor-made environments to meet our needs.
In this demo, we’ll be creating a tracking environment object to follow different states and user actions for analytic purposes.
The first step is to create a struct that will conform to the EnvironmentKey
protocol with only one requirement: a default value.
struct TrackStateKey: EnvironmentKey {
static let defaultValue: TrackStateAction = .init(state: .loading)
}
Then, we need to extend the EnvironmentValues
struct and define a property that will be exposed to our views.
Just like SwiftUI’s built-in environments, we use KeyPath
to access or mutate the default value static property.
extension EnvironmentValues {
var trackState: TrackStateAction {
get { self[TrackStateKey.self] }
set { self[TrackStateKey.self] = newValue }
}
}
Let’s create a TrackState
enum to follow the different states and actions.
enum TrackState {
case loading
case appear(String = .init())
case create(String = .init())
case tap(String = .init())
case error(Error)
}
The TrackStateAction
class encapsulates the core logic of our environment.
class TrackStateAction {
var state: TrackState
init(state: TrackState) {
self.state = state
}
func callAsFunction(_ newState: TrackState) {
state = newState
sendState()
}
private func sendState() {
switch state {
case .appear(let value), .create(let value), .tap(let value):
/// send value to back-end analytics
case .loading:
/// send loading state to back-end analytics
case .error(let error):
/// send error state to back-end analytics
}
}
}
At last, in order to follow specific events such as the creation of a view, we need to create a custom modifier through the View
extension:
extension View {
func trackState(_ newState: TrackState) -> some View {
environment(\.trackState, TrackStateAction(state: newState))
}
}
The environment
function takes two arguments that is your WritableKeyPath
(which is the property from the EnvironmentValues
extension) and the new value.
func environment<V>(_ keyPath: WritableKeyPath<EnvironmentValues, V>, _ value: V) -> some View
We can now use our custom environment object and track app states and actions inside our SwiftUI code.
In a real-life app, each state would be initialized with a specific context such as .tap("Custom view - Log in")
.
struct CustomView: View {
@Environment(\.openURL) var openURL
@Environment(\.trackState) var trackState
var body: some View {
Button {
login()
trackState(.tap())
} label: {
Text("Log in")
}
.trackState(.create())
.onAppear {
trackState(.appear())
}
}
}
To enable analytics for the entire view hierarchy — which is most likely the case — we would inject the environment at the app level.
@main
struct CustomApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.trackState, .init(state: .loading))
}
}
}
Conclusion
By leveraging SwiftUI’s powerful APIs, @Environment
objects allow us to create an elegant way of encapsulating common behaviours within the app.