Custom Property Wrappers for SwiftUI

As I’ve been working with SwiftUI, I’ve come up with some custom property wrappers that I want to share with you. Most of these are property wrappers that can be easily accomplished using other approaches, but I like these for their expressivity.

Making Custom Property Wrappers

The easiest way to make a custom property wrapper that triggers SwiftUI view updates is to simply wrap another property wrapper:

@propertyWrapper
public struct ReinventedState<T>: DynamicProperty {
    @State var wrappedValue: T
}

There’s one crucial piece to make this work, and that’s to declare that your property wrapper conforms to DynamicProperty. If you do this, then the SwiftUI runtime will discover your property wrapper instance and start tracking it for changes. Since your property wrapper is using other built-in property wrappers, SwiftUI will recognize that your wrapper is dependent on the others, and when those update, yours will too.

There is an update() method that’s part of the DynamicProperty protocol, but you typically don’t need to implement it (the default implementation does nothing) unless your wrapper is tracking some internal state that it needs to update before the .wrappedValue can be invoked in a var body somewhere. update() is called right before the view’s body is called.

But this is really all there is to it.

@Feature

One of the most useful property wrappers I’ve made is a simple “feature flag” trigger. It’s fairly common to have feature flags to control whether app-global features are available. Some apps go so far as to download a set of feature flag values and use those to dynamically adjust behavior. I like it for doing this like enabling a hidden debug menu.

The idea behind @Feature is that I can use it to track a particular key path on a Features singleton (or environment object) and then when that feature flag changes, the @Feature wrapper triggers a change at the view level. It looks something like this:

public class Features: ObservableObject {
    public static let shared = Features()
    private init() { }

    @Published public var isDebugMenuEnabled = false
}

@propertyWrapper
public struct Feature<T>: DynamicProperty {
    @ObservedObject private var features: Features

    private let keyPath: KeyPath<Features, T>
    
    public init(_ keyPath: KeyPath<Features, T>, features: Features = .shared) {
        self.keyPath = keyPath
        self.features = features
    }

    public var wrappedValue: T {
        return features[keyPath: keyPath]
    }
}

Now in a view, I can use it like so:

struct AppSettingsView: View {
    @Feature(\.isDebugMenuEnabled) var showDebugMenu

    var body: some View {
        Form {
            if showDebugMenu {
                NavigationLink(destination: DebugSettings()) { Text("Debug Settings") }
            }
            
        }
    }
}

Now any time the Features object publishes a change from anywhere in the app, my @Feature wrapper will pick it up, and the AppSettingsView will be automatically re-built (if it’s on-screen).

A couple notes about this:

  • Yes, this is a singleton and singletons are “bad”. It’s illustrative here. You could just as easily use an @EnvironmentObject instead of an @ObservedObject in the implementation and you’d get the same result
  • Since the entire Features object is being observed, then technically a change to any feature will trigger a change to every @Feature wrapper. You could imagine working around this by doing something clever with specializing on T: Equatable and checking for differences on a particular key path before triggering a change. If you did this, you wouldn’t be observing the Features type directly, but some sort of object that wraps it and re-publishes a particular value.
  • @Feature relies on a KeyPath to know what value to pull out. This guarantees some type-safety, but it also means I don’t end up with stringly-typed keys scattered around my app (more on this shortly).
  • By using @Feature instead of observing the Features value directly with @EnvironmentObject or something, it’s easier to search through the a codebase to find all uses of a feature: I can search for @Feature or .isDebugMenuEnabled and be confident that I’ll have found everything.
  • Strongly-typed key paths means that if I use the Refactor command to renamed a property, it gets changed automatically everywhere throughout the app.

AppSetting and SceneSetting

@AppSetting and @SceneSetting are two that are nearly identical in construction to @Feature, but differ only in where they pull their values from. @Feature relies on a Features class, whereas @AppSetting and @SceneSetting would rely on AppSettings and SceneSettings instances, respectively.

I won’t go too much into the code here, beyond saying that I built these because I don’t like scattering the string keys for UserDefaults values everywhere, and I much prefer the type safety of using key paths.

So, I built an AppSettings class:

public class AppSettings: ObservableObject {

    @AppStorage("appSetting1") public var appSetting1: Int = 42
    @AppStorage("appSetting2") public var appSetting2: String = "Hello"
    

}

and a SceneSettings class:

public class SceneSettings: ObservableObject {

    @SceneStorage("sceneValue1") public var sceneSetting1: Int = 54
    @SceneStorage("sceneValue2") public var sceneSetting2: String = "World"
    

}

Their respective property wrappers are basically identical to @Feature, but obviously use a key path rooted in the corresponding type, and then use an @ObservedObject/@EnvironmentObject of that type. They also implement the projectedValue property so that I can create bindings to use these values using the $… syntax.

struct SomeDetailView: View {
    @SceneSetting(\.shouldShowThatOneInfoSection) var showInfo

    var body: some View {
        DisclosureGroup("Detail Info", isExpanded: $showInfo) {
            SomeInfoView()
        }
        
    }
}

Like with @Feature, these have the same benefits of type safety, refactorability, and avoiding stringly-typed keys.

Other One-Off Wrappers

I’ll sometimes create one-off property wrappers to access specific things in the environment or specific global values. I do these mainly to draw attention to their usage in a particular view. For example, it can be easier to scan through a view and spot an @OpenURL var openURL value than it is to scan through a list of a handful of @Environment(\.…) values and notice the right key path.

When deciding what to use, I always try to err on the side of readability. The compiler doesn’t care what I type, as long as it can correctly generate the executable code. So when deciding what to do, I remember that more often than not, I’m writing this for me, not for the compiler. Therefore, I write it in a way that will (hopefully) make the most sense to me when I come back to this code in weeks, months, or years and have inevitably forgotten what it does.

Where We Go From Here

This is a quick overview of creating custom property wrappers that trigger SwiftUI view updates, and a couple of examples of some that I’ve found to be useful.

The thing I really want to share requires having a bit of background around creating custom property wrappers. I call it @Query and it is how I follow my Laws of Core Data in SwiftUI.


Related️️ Posts️

Core Data and SwiftUI