Using a Swift macro to generate an EnvironmentKey


You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿


I want to show you a Swift macro that's gonna save you so much time when you write SwiftUI code!

If you use SwiftUI, you've probably used the Environment, because it's a very convenient way to inject something into the view hierarchy.

For instance, here my ContentView uses the Environment to fetch a userName and display it on the screen:

And the userName is injected at a higher level in the view hierarchy, here at the Preview level:

But if you’ve used the Environment, you also know that this simplicity comes at the cost of a bit of boilerplate code.

Because if we take a look at this other Swift file, we can see that we've also had to define what SwiftUI calls an EnvironmentKey:

This key is what allows us to store something into the Environment, and you can see that there's quite a lot of boilerplate code involved!

We need to define a struct that conforms to the protocol EnvironmentKey, where we define the type of the property and its defaultValue.

And then we also need to write an extension on EnvironmentValues, where we define a computed property, which will provide us the KeyPath that we will use to query the value from the Environment.

Every time that we want to store a new value into the Environment, we're going to have to write code that's similar to this one, and it will always have the same structure.

The only thing that will change will be:

  • the name of the property

  • its type

  • its default value

Obviously writing this kind of boilerplate code by hand is no fun at all, and it would be amazing if we had a way to automate it.

As you can imagine, this is where a Swift macro can shine!

As it turns out, someone in the iOS community has created the perfect macro to automatically generate all of this boilerplate code for us 👌

So let's rewrite this code, but this time using this Swift macro.

We’ll start by importing the package with the Swift macro, which is called SwiftUIMacros.

Then we’re going to write the code I need in order to use the macro:

As you can see, I'm only writing the minimal amount of code needed to define the new value that I want to store into the Environment:

  • the name of the property

  • its type

  • a default value.

And now it's time to actually use the macro!

The macro is called @EnvironmentStorage and we've applied it to the extension.

In order to see what it does, we are going to expand it:

This will show us the Swift code that is generated by this macro.

As you can see the macro @EnvironmentStorage adds another macro called @EnvironmentValue on each property defined inside the extension of EnvironmentValues.

So now we of course also want to expand the macro @EnvironmentValue:

And this time we can see the boilerplate code being generated 🎉

What’s amazing is that this time we didn't need to write this code: the macro did it automatically for us at compile time.

As I was mentioning earlier, this macro is not available by default in Xcode: it's been implemented by someone in the iOS community and the code is available on GitHub.

What you need to do in order to use it is simply to add the Swift package to your project just like you would with any regular Swift package 👌

That’s all for this article!

I hope that this trick will help you save a lot of time 😌

Here’s the code if you want to experiment with it:

// Without the macro
import SwiftUI

struct UserNameEnvironmentKey: EnvironmentKey {
    static var defaultValue: String = "Anonymous"
}

extension EnvironmentValues {
    var userName: String {
        get {
            self[UserNameEnvironmentKey.self]
        }
        
        set {
            self[UserNameEnvironmentKey.self] = newValue
        }
    }
}

// With the macro
import SwiftUI
import SwiftUIMacros

@EnvironmentStorage
extension EnvironmentValues {
    var userName: String = "Anonymous"
}
Previous
Previous

Swift has more formatters than you know!

Next
Next

Do you know what .layoutIfNeeded() actually does?