Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

@AppStorage explained and replicated for a better alternative

The @AppStorage Property Wrapper was introduced in SwiftUI to allow easy access to the user defaults. When property wrappers were introduced, many examples used a user defaults wrapper as an example. It’s a common use case where you can move boilerplate code into a wrapper.

Although the @AppStorage wrapper is working out great in many cases, it has a few downsides when used across an app. While developing my Simulator development tool RocketSim, I found that it was hard to keep multiple instances of the wrapper in sync as it takes a string input key. Keeping those strings in sync across the project was challenging, for which I decided it was time for a fun project.

How to use @AppStorage

Before we dive into an alternative approach, I would first explain how the app storage property wrapper works in SwiftUI.

If you’re new to Property Wrappers in Swift, I recommend first reading Property Wrappers in Swift explained with code examples.

The following code shows how you can create a boolean value that is stored in the user defaults:

@AppStorage("should_show_hello_world", store: .standard) var shouldShowHelloWorld: Bool = false

As you can see, we can initialize it with both a key and a store if we want to change the used store. Within a SwiftUI view, this could look as follows:

struct AppStorageContentView: View {
    
    @AppStorage("should_show_hello_world", store: .standard) var shouldShowHelloWorld: Bool = false
    
    var body: some View {
        VStack {
            Text(shouldShowHelloWorld ? "Hello, world!" : "")
                .padding()
            Button("Change text") {
                shouldShowHelloWorld.toggle()
            }
            Toggle("Change text", isOn: $shouldShowHelloWorld)
        }
    }
}

The app storage property wrapper has a projected value providing a binding that you can use with SwiftUI controls.

Overriding the default app storage container

It can be inconvenient to define the user defaults store for each app storage property wrapper. To simplify this, we could use the defaultAppStorage view modifier:

struct AppStorageContentView: View {
    
    @AppStorage("should_show_hello_world") var shouldShowHelloWorld: Bool = false
    
    var body: some View {
        VStack {
            Text(shouldShowHelloWorld ? "Hello, world!" : "")
                .padding()
            Button("Change text") {
                shouldShowHelloWorld.toggle()
            }
            Toggle("Change text", isOn: $shouldShowHelloWorld)
        }.defaultAppStorage(.standard)
    }
}

Downsides to using the @AppStorage Property Wrapper

The above code examples might work great for your app. Especially when you’re configuring user defaults settings from one view only, there’s no real need to revisit this pattern. In my case, however, I’ve had to define user preferences that can be read and changed throughout the app.

I found the following downsides to being true for using the app storage property wrapper:

  • Changing the user defaults store for all @AppStorage wrapper instances isn’t straightforward
  • Using a String input is error prone as there’s no easy way to force keeping those keys in sync throughout a project
  • Available preference keys are not easy to be discovered
  • There’s no obvious way of monitoring changes outside of the view that contains the property wrapper. What if I want to update the app after a user changed a preference?

With these downsides in mind, I started defining a list of requirements:

  • All preferences should be defined in a single place, both for discoverability and readability.
  • A key-path based initialiser should make keys discoverable through autocompletion when used in a new property wrapper
  • It should be possible to monitor each preference key individually, to only update views if a related key changed
  • Feature parity should be realised for the property wrapper replacing the @AppStorage wrapper. This mostly means providing a binding through the projected value

Creating an alternative solution for reading and writing user defaults through a property wrapper

As the downsides and requirements are clear, it’s time to dive into my custom solution. The solution will contain many of Swift’s latest features, which I won’t always cover in detail. Instead, I’ll link to relevant articles explaining them to you in detail if you want to know more.

Defining the preferences container

We start by defining the preferences container. This will be the source of input for our @AppStorage property wrapper replacement.

final class Preferences {
    
    static let standard = Preferences(userDefaults: .standard)
    fileprivate let userDefaults: UserDefaults
    
    /// Sends through the changed key path whenever a change occurs.
    var preferencesChangedSubject = PassthroughSubject<AnyKeyPath, Never>()
    
    init(userDefaults: UserDefaults) {
        self.userDefaults = userDefaults
    }
    
    @UserDefault("should_show_hello_world")
    var shouldShowHelloWorld: Bool = false
}

The preferences container defines a static accessor for the standard store. A passthrough subject is used for providing a stream of changed key paths, which we can later use to monitor changes. Our shouldShowHelloWorld property is redefined and uses the @UserDefault property wrapper that I introduced in my article Property Wrappers in Swift explained with code examples.

Improving the @UserDefault property wrapper

Taking this @UserDefault property wrapper as a starting point, we can improve it by sending changed key paths through the passthrough subject defined on the preferences container. We can do this by accessing the enclosing instance through a custom subscript.

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value

    var wrappedValue: Value {
        get { fatalError("Wrapped value should not be used.") }
        set { fatalError("Wrapped value should not be used.") }
    }
    
    init(wrappedValue: Value, _ key: String) {
        self.defaultValue = wrappedValue
        self.key = key
    }
    
    public static subscript(
        _enclosingInstance instance: Preferences,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<Preferences, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<Preferences, Self>
    ) -> Value {
        get {
            let container = instance.userDefaults
            let key = instance[keyPath: storageKeyPath].key
            let defaultValue = instance[keyPath: storageKeyPath].defaultValue
            return container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            let container = instance.userDefaults
            let key = instance[keyPath: storageKeyPath].key
            container.set(newValue, forKey: key)
            instance.preferencesChangedSubject.send(wrappedKeyPath)
        }
    }
}

Accessing the enclosing instance through this subscript is a feature that’s still in review. It was part of the original Property Wrapper proposal and it’s yet to be seen whether it will make it as an accepted feature. Therefore, use this feature at your own risk.

Once again, this is a simplified version of the property wrapper from my other article. We’ll use it as a base to explain my custom solution.

There are two major differences compared to the original @UserDefault property wrapper:

  • The user defaults is now read from the preferences container. This allows us to initialise the preferences container with a given user defaults storage, and directly use it for each property. No need to define it per property anymore
  • The subscript setter sends the changed key-path through the passthrough subject, allow us to later on monitor changes for specific keys

Creating the @AppStorage alternative property wrapper

Now that we’ve defined the preferences container and its properties, we can start creating an accessor for these preferences using a key-path input. The outcome within a SwiftUI view looks as follows:

struct ContentView: View {
    
    @Preference(\.shouldShowHelloWorld) var shouldShowHelloWorld
    
    var body: some View {
        VStack {
            Text(shouldShowHelloWorld ? "Hello, world!" : "")
                .padding()
            Button("Change text") {
                shouldShowHelloWorld.toggle()
            }
            Toggle("Change text", isOn: $shouldShowHelloWorld)
        }
    }
}

As you can see, we defined a new @Preference property wrapper which is initialized using a key-path referencing our shouldShowHelloWorld property. We don’t even have to define the output type as that’s derived from the preferences container.

The @Preference property wrapper replacing the @AppStorage wrapper looks as follows:

@propertyWrapper
struct Preference<Value>: DynamicProperty {
    
    @ObservedObject private var preferencesObserver: PublisherObservableObject
    private let keyPath: ReferenceWritableKeyPath<Preferences, Value>
    private let preferences: Preferences
    
    init(_ keyPath: ReferenceWritableKeyPath<Preferences, Value>, preferences: Preferences = .standard) {
        self.keyPath = keyPath
        self.preferences = preferences
        let publisher = preferences
            .preferencesChangedSubject
            .filter { changedKeyPath in
                changedKeyPath == keyPath
            }.map { _ in () }
            .eraseToAnyPublisher()
        self.preferencesObserver = .init(publisher: publisher)
    }

    var wrappedValue: Value {
        get { preferences[keyPath: keyPath] }
        nonmutating set { preferences[keyPath: keyPath] = newValue }
    }

    var projectedValue: Binding<Value> {
        Binding(
            get: { wrappedValue },
            set: { wrappedValue = $0 }
        )
    }
}

This wrapper makes use of quite some Swift features to replicate the @AppStorage behavior. Let’s go over them one by one.

The wrapped value is a simple proxy between the underlying preferences container and uses the key path as input for reading and writing.

The projected value replicates the @AppStorage behavior by providing a binding for SwiftUI views.

The wrapper inherits from DynamicProperty which allows us to inform SwiftUI to redraw a view when a change occurred. The DynamicProperty protocol requires us to define an input for the change, which can be any instance you would use in regular SwiftUI views to trigger a redraw. In this case, we decided to use an observable object.

This observable object is custom too, where we use an instance to change a Combine publisher into an observable object.

If you’re new to Combine, I recommend reading Getting started with the Combine framework in Swift.

final class PublisherObservableObject: ObservableObject {
    
    var subscriber: AnyCancellable?
    
    init(publisher: AnyPublisher<Void, Never>) {
        subscriber = publisher.sink(receiveValue: { [weak self] _ in
            self?.objectWillChange.send()
        })
    }
}

This wrapper can be useful whenever you want to have a publisher as input for triggering a SwiftUI change. In this case, we’re using the passthrough subject from our preferences container as an input to trigger changes in our dynamic @Preference property wrapper.

Using the custom alternative

Altogether, this solution allows us to use the property wrapper as follows:

struct ContentView: View {
    
    @Preference(\.shouldShowHelloWorld) var shouldShowHelloWorld
    
    var body: some View {
        VStack {
            Text(shouldShowHelloWorld ? "Hello, world!" : "")
                .padding()
            Button("Change text") {
                shouldShowHelloWorld.toggle()
            }
            Toggle("Change text", isOn: $shouldShowHelloWorld)
        }
    }
}

We can access the projected value, changed the preference value from our views, and update our view when the preference is changed from a different place.

We can also access the passthrough subject directly on the preferences container to observe specific changes from within other instances:

Preferences.standard
    .preferencesChangedSubject
    .filter { changedKeyPath in
        changedKeyPath == \Preferences.shouldShowHelloWorld
    }.sink { _ in
        print("Should show hello world preference changed!")
    }

The final solution gives your projects more flexibility, consistency, and discoverability when using preferences throughout your app. It has been a game-changer for RocketSim‘s performance as I couldn’t monitor specific keys only. In other words, some views were refreshed for unrelated preference keys that changed.

Conclusion

The @AppStorage property wrapper is a great addition to SwiftUI and allows nicely accessing the user defaults. In some cases, this wrapper might be enough. However, if your app uses preferences consistently throughout your project, it’s worth considering the alternative approach, which improves readability, discoverability, and consistency. Many of Swift’s features are combined, resulting in a dynamic solution fitting our needs.

If you like to improve your Swift knowledge even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!

 

Stay Updated with the Latest in Swift

You don't need to master everything, but staying informed is crucial. Join our community of 18,250 developers and stay ahead of the curve:


Featured SwiftLee Jobs

Find your next Swift career step at world-class companies with impressive apps by joining the SwiftLee Talent Collective. I'll match engineers in my collective with exciting app development companies. SwiftLee Jobs