Writing custom property wrappers for SwiftUI

Published on: January 16, 2022

It's been a while since I published my post that helps you wrap your head around Swift's property wrappers. Since then, I've done more and more SwiftUI related work and one challenge that I recently had to dig into was passing dependencies from SwiftUI's environment into a custom property wrapper.

While figuring this out I learned about the DynamicProperty protocol which is a protocol that you can conform your property wrappers to. When your property wrapper conforms to the DynamicProperty protocol, your property wrapper will essentially become a part of your SwiftUI view. This means that your property wrapper can extract values from your SwiftUI environment, and your SwiftUI view will ask your property wrapper to update itself whenever it's about to evaluate your view's body. You can even have @StateObject properties in your property wrapper to trigger updates in your views.

This protocol isn't limited to property wrappers, but for the sake of this post I will explore DynamicProperty in the context of a property wrapper.

Understanding the basics of DynamicProperty

Defining a dynamic property is as simple as conforming a property wrapper to the DynamicProperty protocol. The only requirement that this protocol has is that you implement an update method that's called by your view whenever it's about to evaluate its body. Defining such a property wrapper looks as follows:

@propertyWrapper
struct MyPropertyWrapper: DynamicProperty {
  var wrappedValue: String

  func update() {
    // called whenever the view will evaluate its body
  }
}

This example on its own isn't very useful of course. I'll show you a more useful custom property wrapper in a moment. Let's go ahead and take a moment to explore what we've defined here first.

The property wrapper that we've defined here is pretty straightforward. The wrapped value for this wrapper is a string which means that it'll be used as a string in our SwiftUI view. If you want to learn more about how property wrappers work, take a look at this post I published on the topic.

There's also an update method that's called whenever SwiftUI is about to evaluate its body. This function allows you to update state that exists external to your property wrapper. You’ll often find that you don’t need to implement this method at all unless you’re doing a bunch of more complex work in your property wrapper. I’ll show you an example of this towards the end of the article.

⚠️ Note that I’ve defined my property wrapper as a struct and not a class. Defining a DynamicProperty property wrapper as a class is allowed, and works to some extent, but in my experience it produces very inconsistent results and a lot of things you might expect to work don’t actually work. I’m not quite sure exactly why this is the case, and what SwiftUI does to break property wrappers that were defined as classes. I just know that structs work, and classes don’t.

One pretty neat thing about a DynamicProperty is that SwiftUI will pick it up and make it part of the SwiftUI environment. What this means is that you have access to environment values, and that you can leverage property wrappers like @State and @ObservedObject to trigger updates from within your property wrapper.

Let’s go ahead and define a property wrapper that hold on to some state and updates the view whenever this state changes.

Triggering view updates from your dynamic property

Dynamic properties on their own can’t tell a SwiftUI view to update. However, we can use @State, @ObservedObject, @StateObject and other SwiftUI property wrappers to trigger view updates from within a custom property wrapper.

A simple example would look a little bit like this:

@propertyWrapper
struct CustomProperty: DynamicProperty {
    @State private var value = 0

    var wrappedValue: Int {
        get {
            return value
        }

        nonmutating set {
            value = newValue
        }
    }
}

This property wrapper wraps an Int value. Whenever this value receives a new value, the @State property value is mutated, which will trigger a view update. Using this property wrapper in a view looks as follows:

struct ContentView: View {
    @CustomProperty var customProperty

    var body: some View {
        Text("Count: \(customProperty)")

        Button("Increment") {
            customProperty += 1
        }
    }
}

The SwiftUI view updates the value of customProperty whenever a button is tapped. This on its own does not trigger a reevaluation of the body. The reason our view updates is because the value that represents our wrapped value is marked with @State. What’s neat about this is that if value changes for any reason, our view will update.

A property wrapper like this is not particularly useful, but we can do some pretty neat things to build abstractions around different kinds of data access. For example, you could build an abstraction around UserDefaults that provides a key path based version of AppStorage:

class SettingKeys: ObservableObject {
    @AppStorage("onboardingCompleted") var onboardingCompleted = false
    @AppStorage("promptedForProVersion") var promptedForProVersion = false
}

@propertyWrapper
struct Setting<T>: DynamicProperty {
    @StateObject private var keys = SettingKeys()
    private let key: ReferenceWritableKeyPath<SettingKeys, T>

    var wrappedValue: T {
        get {
            keys[keyPath: key]
        }

        nonmutating set {
            keys[keyPath: key] = newValue
        }
    }

    init(_ key: ReferenceWritableKeyPath<SettingKeys, T>) {
        self.key = key
    }
}

Using this property wrapper would look as follows:

struct ContentView: View {
    @Setting(\.onboardingCompleted) var didOnboard

    var body: some View {
        Text("Onboarding completed: \(didOnboard ? "Yes" : "No")")

        Button("Complete onboarding") {
            didOnboard = true
        }
    }
}

Any place in the app that uses @Setting(\.onboardingCompleted) var didOnboard will automatically update when the value for onboarding completed in user defaults updated, regardless of where / how this happened. This is exactly the same as how @AppStorage works. In fact, my custom property wrapper relies heavily on @AppStorage under the hood.

My SettingsKeys object wraps up all of the different keys I want to write to in UserDefaults, the @AppStorage property wrapper allows for easy observation and makes it so that I can define SettingsKeys as an ObservableObject without any troubles.

By implementing a custom get and set on my Setting<T>'s wrappedValue, I can easily read values from user defaults, or write a new value simply by assigning to the appropriate key path in SettingsKeys.

Simple property wrappers like these are very useful when you want to streamline some of your data access, or if you want to add some semantic meaning and ease of discovery to your property wrapper.

To see some more examples of simple property wrappers (including a user defaults one that inspired the example you just saw), take a look at this post from Dave Delong.

Using SwiftUI environment values in your property wrapper

In addition to triggering view updates via some of SwiftUI’s property wrappers, it’s also possible to access the SwiftUI environment for the view that your property wrapper is used in. This is incredibly useful when your property wrapper is more complex and has a dependency on, for example, a managed object context, a networking object, or similar objects.

Accessing the SwiftUI environment from within your property wrapper is done in exactly the same way as you do it in your views:

@propertyWrapper
struct CustomFetcher<T>: DynamicProperty {
    @Environment(\.managedObjectContext) var managedObjectContext

    // ...
}

Alternatively, you can read environment objects that were assigned through your view with the .environmentObject view modifier as follows:

@propertyWrapper
struct UsesEnvironmentObject<T: ObservableObject>: DynamicProperty {
    @EnvironmentObject var envObject: T

    // ...
}

We can use the environment to conveniently pass dependencies to our property wrappers. For example, let’s say you’re building a property wrapper that fetches data from the network. You might have an object named Networking that can perform network calls to fetch the data you need. You could inject this object into the property wrapper through the environment:

@propertyWrapper
struct FeedLoader<Feed: FeedType>: DynamicProperty {
    @Environment(\.network) var network

    var wrappedValue: [Feed.ObjectType] = []
}

The network environment key is a custom key that I’ve added to my SwiftUI environment. To learn more about adding custom values to the SwiftUI environment, take a look at this tip I posted earlier.

Now that we have this property wrapper defined, we need a way to fetch data from the network and assign it to something in order to update our wrapped value. To do this, we can implement the update method that allows us to update data that’s referenced by our property wrapper if needed.

Implementing the update method for your property wrapper

The update method that’s part of the DynamicProperty protocol allows you to respond to SwiftUI’s body evaluations. Whenever SwiftUI is about to evaluate a view’s body, it will call your dynamic property’s update method.

💡 To learn more about how and when SwiftUI evaluates your view’s body, take a look at this post where I explore body evaluation in depth.

As mentioned earlier, you won’t often have a need to implement the update method. I’ve personally found this method to be handy whenever I needed to kick off some kind of data fetching operation. For example, to fetch data from Core Data in a custom implementation of @FetchRequest, or to experiment with fetching data from a server. Let’s expand the FeedLoader property wrapper from earlier a bit to see what a data loading property wrapper might look like:

@propertyWrapper
struct FeedLoader<Feed: FeedType>: DynamicProperty {
    @Environment(\.network) var network
    @State private var feed: [Feed.ObjectType] = []
    private let feedType: Feed
    @State var isLoading = false

    var wrappedValue: [Feed.ObjectType] {
        return feed
    }

    init(feedType: Feed) {
        self.feedType = feedType
    }

    func update() {
        Task {
            if feed.isEmpty && !isLoading {
                self.isLoading = true
                self.feed = try await network.load(feedType.endpoint)
                self.isLoading = false
            }
        }
    }
}

This is a very, very simple implementation of a property wrapper that uses the update method to go to the network, load some data, and assign it to the feed property. We have to make sure that we only load data if we’re not already loading, and we also need to check whether or not the feed is currently empty to avoid loading the same data every time SwiftUI decides to re-evaluate our view’s body.

This of course begs the question, should you use a custom property wrapper to load data from the network? And the answer is, I’m not sure. I’m still heavily experimenting with the update method, its limitations, and its benefits. One thing that’s important to realize is that update is called every time SwiftUI is about to evaluate a view’s body. So synchronously assigning a new value to an @State property from within that method is probably not the best idea; Xcode even shows a runtime warning when I do this.

At this point I’m fairly sure Apple intended update to be used for updating or reading state external to the property wrapper rather than it being used to synchronously update state that’s internal to the property wrapper.

On the other hand, I’m positive that @FetchRequest makes heavy use of update to refetch data whenever its predicate or sort descriptors have changed, probably in a somewhat similar way that I’m fetching data from the network here.

Summary

Writing custom property wrappers for your SwiftUI views is a fun exercise and it’s possible to write some very convenient little helpers to improve your experience while writing SwiftUI views. The fact that your dynamic properties are connected to the SwiftUI view lifecycle and environment is super convenient because it allows you to trigger view updates, and read values from SwiftUI’s environment.

That said, documentation on some of the details surrounding DynamicProperty is severely lacking which means that we can only guess how mechanisms like update() are supposed to be leverages, and why structs work perfectly fine as dynamic properties but classes don’t.

These are some points that I hope to be able to expand on in the future, but for now, there are definitely still some mysteries for me to unravel. And this is where you come in! If you have any additions for this posts, or if you have a solid understanding of how update() was meant to be used, feel free to send me a Tweet. I’d love to hear from you.

Categories

SwiftUI