Simplifying Environment Management in SwiftUI with SwiftEnvironment

Nayanda Haberty
Nerd For Tech
Published in
4 min readApr 18, 2024

In SwiftUI, the Environment is a crucial feature that allows you to fine-tune your views with configuration and data. Whether it’s adjusting layout direction, customizing accessibility options, or tweaking locale settings, the Environment has your back. However, managing environment values in SwiftUI can sometimes be a bit cumbersome, especially when it comes to defining and injecting dependencies. That’s where SwiftEnvironment comes in.

Using SwiftUI Environment

Let’s start by looking at how we can manage environment values in SwiftUI without SwiftEnvironment. Imagine we have a protocol called MyDependency that we want to use as an environment value. Here's how we might go about it:

protocol MyDependency {
func doServiceStuff()
}

final class MyDependencyDefault {
func doServiceStuff() { }
}

struct MyDependencyEnvironmentKey: EnvironmentKey {
static let defaultValue: MyDependency = MyDependencyDefault()
}

extension EnvironmentValues {
var myDependency: MyDependency {
get { self[MyDependencyEnvironmentKey.self] }
set { self[MyDependencyEnvironmentKey.self] = newValue }
}
}

In this approach, we define a default object and use it as the default value for our environment key. We then access this value using the “@Environment” property wrapper in our views.

struct MyView: View {
@Environment(\.myDependency) var myDependency
@StateObject var viewModel: MyViewModel

var body: some View {
// Your View body code here
}
}

While the real dependency will be injected like this:

MyView(viewModel: MyViewModel())
.environment(\.myDependency, MyRealDependency())

Introducing SwiftEnvironment

Enter SwiftEnvironment — a library designed to simplify environment value management in SwiftUI. With SwiftEnvironment, defining and injecting environment values becomes simple. Let’s see how we can use SwiftEnvironment to achieve the same result with less boilerplate.

@Stubbed(type: .class)
protocol MyDependency {
func doServiceStuff()
}

@EnvironmentValue
public extension EnvironmentValues {
static let myDependency: MyDependency = MyDependencyStub()
}

With just a few lines of code, we can define our environment value using the “@EnvironmentValue” and “@Stubbed” macros provided by SwiftEnvironment. These macros handle the generation of boilerplate code for us, making the process much more straightforward.

@Stubbed Macro

The “@Stubbed” macro simplifies the process of creating stubs for protocols in Swift. When you apply “@Stubbed” to a protocol, SwiftEnvironment automatically generates a stub class that conforms to that protocol. This stub class includes default implementations for all the methods and properties defined in the protocol, allowing you to use it as a placeholder or default value for dependency injection.

@EnvironmentValue Macro

The “@EnvironmentValue” macro simplifies the process of defining environment values in SwiftUI. When you apply “@EnvironmentValue” to an extension of EnvironmentValues, SwiftEnvironment generates the necessary boilerplate code to make the specified value available as an environment keypath.

GlobalEnvironment with SwiftEnvironment

Sometimes, you need a dependency to be available globally across your entire app. This could be the case for things like authentication tokens, user preferences, or analytics services. While you could use a singleton pattern to achieve this, it’s not always the best approach, especially if you’re aiming for a modular and testable architecture.

That’s where SwiftEnvironment’s GlobalEnvironment feature comes into play. Similar to SwiftUI's Environment property wrapper, but with a global scope, GlobalEnvironment allows you to inject dependencies into a global resolver, making them accessible from anywhere in your app.

Let’s see how it works:

struct MyView: View {

@GlobalEnvironment(\.myDependency) var myDependency
@StateObject var viewModel: MyViewModel

var body: some View {
// Your View body code here
}
}

In this example, we’re using the “@GlobalEnvironment” property wrapper to inject the myDependency into our MyView. Behind the scenes, SwiftEnvironment stores this dependency in a global resolver, making it available throughout the app.

Injecting the dependency is just as simple:

GlobalResolver.environment(\.myDependency, MyRealDependency())

With a single line of code, we can inject our dependency into the global resolver. From that point on, any part of our app can access the dependency using the “@GlobalEnvironment” property wrapper.

By using the GlobalResolver, we can inject our dependency and make it accessible globally throughout our app. This makes it easy to access environment values from places like ViewModels, even in modular architectures.

Just like in this example:

final class MyViewModel{ 

@GlobalEnvironmnent(\.myDependency) var myDependency

// Your code here
}

During unit testing, this property wrapper is settable, allowing you to mock dependencies easily.

 viewModelUnderTest.myDependency = myDependencyMock

In summary, GlobalEnvironment in SwiftEnvironment provides a powerful way to manage global dependencies in your SwiftUI app, promoting modularity, testability, and flexibility. Whether you're building a small app or a large-scale project, GlobalEnvironment can help you keep your code clean, organized, and easy to maintain.

Conclusion

With SwiftEnvironment, managing environment values in SwiftUI becomes simpler and more efficient. By leveraging its macros and utilities, we can streamline the process of defining and injecting dependencies, saving time and reducing boilerplate code. Whether you’re working with SwiftUI Environment or need global access to your dependencies, SwiftEnvironment has you covered.

Feedback and Contributions

We’d love to hear your feedback on SwiftEnvironment. If you encounter any issues, have feature requests, or want to contribute to the project, please visit the GitHub repository and open an issue or pull request. Your contributions help make SwiftEnvironment better for everyone!

--

--

Nayanda Haberty
Nerd For Tech

I love programming and learning. Expert in iOS, but also did Android, Backend, and Web Dev. Programming is fun and I enjoy exploring different tech stacks.