After I wrote and released Foil, my library for implementing a property wrapper for UserDefaults, one of the criticisms on Twitter was that a mechanism for observing such properties should have been included. I disagreed. In the post I argued that this was easy enough for clients to handle on their own, but more importantly that there are too many options for how to do this and I didn’t think Foil should impose any one of them on clients.

Shortly after releasing Foil, Basem Emara opened an issue to ask about observing property changes using Combine. No code changes were required for the library, and instead we merged some updates to the documentation to explain how to use Foil and Combine together. This gave me confidence that I made the correct choice to omit observation in Foil.

Today, I want to review the various methods for observing properties in Swift. I will use Foil for the example code in this post. Consider the following snippet, where we define some settings for our app.

final class AppSettings {
    static let shared = AppSettings()

    @WrappedDefault(keyName: "flagEnabled", defaultValue: false)
    var flagEnabled: Bool
}

What are our options for observing the flagEnabled property?

Swift property observers

The simplest but least flexible way to observe properties is built-in to the Swift language itself. We can add Swift’s property observers willSet and didSet.

@WrappedDefault(keyName: "flagEnabled", defaultValue: false)
var flagEnabled: Bool {
    willSet {
        print("will set")
    }

    didSet {
        print("did set")
    }
}

Observing changes this way would make the most sense if you decentralize your @WrappedDefault properties by defining them on classes that utilize them, rather than a shared AppSettings class. For example, you could add a @WrappedDefault property to a view controller. To make use of the property observers within a centralized class like AppSettings, you would need to pass in a closure to execute in the observers or post a Notification — neither of which are ideal.

Key-Value Observing

The next option is to use key-value observing from Foundation, which has new and improved Swift APIs that utilize Key-Path expressions. This works well with a centralized AppSettings class. However, it requires inheriting from NSObject and making the properties that you wish to observe @objc and dynamic.

final class AppSettings: NSObject {
    static let shared = AppSettings()

    @WrappedDefault(keyName: "flagEnabled", defaultValue: false)
    @objc dynamic var flagEnabled: Bool
}

Then, from elsewhere in our code we can observe changes:

let observer = AppSettings.shared.observe(\.flagEnabled, options: [.new]) { settings, change in
    print("property changed")
}

Combine

Similar to KVO, we can use a Publisher if our app is using Combine.

var cancellable = Set<AnyCancellable>()

AppSettings.shared
    .publisher(for: \.flagEnabled, options: [.new])
    .sink { newValue in
        print("property changed")
    }
    .store(in: &cancellable)

You can simplify this further by using the @Published propperty wrapper.

Third-party libraries

Finally, there are a handful of open source reactive extension libraries available for Swift that you may already be using. I won’t dive into these, but the implementations and concepts would be similar to using Combine.

Conclusion

These are the primary ways to observe property changes in Swift. I think I made the right decision to omit this feature from Foil and instead allow clients to choose their own method of observation. It takes very little code to implement any of these methods, and you could streamline it even more with a few small extensions.