WOODY'S
FINDINGS

Safer singletons with property wrappers

Tip

22 November 2020

Singleton in programming are a common way to share an object across the overall code base. If they should be used with caution, there are sometimes a nice solution to share a single source of truth. In this article, we will take a look at two use cases where singleton are - according to me - a perfect fit if we update the concept with property wrappers.

Why use singletons?

Most often, it is not recommended to share an object which state can be updated from several places. This leads to many problems and the cost of resolving them can become greater than the benefit of easily access the shared object.
For example, the shared object might have a property that any other object can modify. In this case, this property should be hidden behind a private queue in case two objects read or write this property on different threads.

In this article, we will focus on singletons serving as a single source of truth: a singleton will only be used to share properties which cannot be mutated by other objects. In the last part, we will see how to extend the concept to also wrap an object (here a NSManagedObjectContext) and expose its functions behind a protocol.

Sharing data

Let's take an example with an app which purpose is to edit shared files. For each file, the user can have three types of rights: owner, writer and reader. As you might imagine, those rights will be used to prevent the user from accessing to certain features to edit the file. This value can initially be retrieved by fetching an API, or by decoding a configuration file. The thing is: once it is retrieved, it should not be possible to update its value.
Also, a property lastModificationDate will be similarly retrieved to inform about when the file was last modified.

Let's think about it. It would be useful to be able to let any object know what are the user rights on the file. For example, an EditorViewController might disable some of the buttons on a view to prevent a modification. Also, a navigation bar would use this information to display the rights on the file. The same goes for the lastModificationDate property. It could be used to send the last modifications on the file to an API, and be displayed in an ExportViewController.

Thus, it appears that in this case, a singleton could be a good fit. Let's try to implement it.

enum Rights {
    case owner, writer, reader
}

final class FileConfiguration {

    static let shared = FileConfiguration()

    private(set) var rights = Rights.reader
    private(set) var lastModificationDate: Date?

    private init() {}

    static func setup(configurationURL: URL, sender: AppDelegate) {
        // here the `shared` properties are updated
    }
}

Some remarks

  • The init is private as for any singleton to prevent another piece of code to instantiate a FileConfiguration.
  • The properties are marked as private(set) to not be modified outside the scope of the class
  • The function setup(configurationURL:sender:) should be called at the launch of the app and should update the shared properties with the ones in the configuration at the given URL. This call should not be made by another object but the AppDelegate. Hence the sender parameter to ensure no other object can make this call.

Here is how this FileConfiguration implementation could be used in an EditorViewController

final class EditorViewController: UIViewController {

    let editButton = UIButton()

    func setupEditButton() {
        editButton.isEnabled = [.owner, .writer].contains(FileConfiguration.shared.rights)
    }
}

Having this kind of information is in one single place is great since it will allow us to not duplicate it. If we had to pass the rights property on each object, we might want to update in somewhere thinking that it would be updated in the rest of the app.
But soon enough a problem arises when we want to test. For instance, how can we test that the button is well disabled depending on the rights? And if we want to show an alert to the user when they have only the reader rights? There is now way yet we can inject a mock value inside FileConfiguration.shared.rights.

A simple solution would be to remove the private(set) mark on the rights property. But this would be careless since it would allow any object to make a modification, which could change the behavior of other objects depending on this value.

Another - safer - solution would be to use the FileConfiguration.shared as a default parameter.

final class EditorViewController: UIViewController {

    let editButton = UIButton()

    func setupEditButton(rights: Rights = FileConfiguration.shared.rights) {
        editButton.isEnabled = [.owner, .writer].contains(rights)
    }
}
This is fine but... Should we do that for each function that needs to access FileConfiguration.shared? 😅 As we, developers, are lazy by nature, an improved solution would be to store a reference to the shared instance.
final class EditorViewController: UIViewController {

    var fileConfiguration: FileConfiguration
    var editButton = UIButton()

    init(fileConfiguration: FileConfiguration = .shared) {
        self.fileConfiguration = fileConfiguration
        super.init()
    }

    func setupEditButton() {
        editButton.isEnabled = [.owner, .writer].contains(fileConfiguration.rights)
    }
}
And... the compiler will complain that we are not calling the designated initialiser of UIViewController and that we have to implement the required init?(coder: NSCoder). 🙈 NSObjects do not make the life of developers easier to implement a custom init!
So here is how this could be done.
final class EditorViewController: UIViewController {

    var fileConfiguration: FileConfiguration
    var editButton = UIButton()

    init(fileConfiguration: FileConfiguration = .shared) {
        self.fileConfiguration = fileConfiguration
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupEditButton() {
        editButton.isEnabled = [.owner, .writer].contains(fileConfiguration.rights)
    }
}
And suddenly, we realise that we once again cannot inject a mocked FileConfiguration. It's even worse than before because we have now to inject a FileConfiguration although the class init is private. 😢

In a desperate last attempt, we could write a FileConfigurationProtocol and make FileConfiguration conform to it. We would then have to declare each property we want to access in the protocol, and then provide a mock object that has all those properties, and... Stop! We said we were lazy, right? Surely, we do not want to do that. Also, such an implementation would be hardly maintainable.

Thus, as we face our code, dejected by the fact that we cannot easily inject mock singletons, an untold point appears in the sky! Is it a bird? Is it a plane? No, it's a property wrapper!

A property wrapper comes to rescue

The object we will use as a property wrapper will be a class named FileConfig. It will take a KeyPath to specify the value to be accessed. Then, to access to this value in the shared FileConfiguration, an object will declare a property with this property wrapper: @FileConfig(\.property).
To enforce that, FileConfiguration.shared will be marked as fileprivate and the class FileConfig will be declared in the same file.

final class FileConfiguration {

    fileprivate static let shared = FileConfiguration()

    private(set) var rights = Rights.reader
    private(set) var lastModificationDate: Date?

    private init() {}

    static func setup(configurationURL: URL, sender: AppDelegate) {
        // here the `shared` properties are updated
    }
}

@propertyWrapper final class FileConfig<Value> {

    let keyPath: KeyPath<FileConfiguration, Value>

    var wrappedValue: Value {
        FileConfiguration.shared[keyPath: keyPath]
    }

    init(_ keyPath: KeyPath<FileConfiguration, Value>) {
        self.keyPath = keyPath
    }
}
We can now modify EditorViewController.
final class EditorViewController: UIViewController {

    @FileConfig(\.rights) var rights
    var editButton = UIButton()

    func setupEditButton() {
        editButton.isEnabled = [.owner, .writer].contains(rights)
    }
}
It seems nicer, don't you think? But wait! We cannot inject dependency here. Let's implement that now.

This syntax might reminds you the environment from SwiftUI which is where this idea comes from. But as a side note, I do not think the environment feature uses a singleton.

Dependency injection

To allow dependency injection, we will offer the possibility to override the returned value. This property will be settable only when accessing the projected value of the property wrapper. If you are accustomed to SwiftUI, you know that a projected value is specified by a dollar $ sign on the property name and means that it's the property wrapper itself that should be accessed rather than the wrapped property.

@propertyWrapper final class FileConfig<Value> {

    /// To be set to override the value given by the configuration
    /// when performing dependency injection
    var overrideValue: Value?
    let keyPath: KeyPath<FileConfiguration, Value>

    var wrappedValue: Value {
        overrideValue ?? FileConfiguration.shared[keyPath: keyPath]
    }

    var projectedValue: FileConfig { self }

    init(_ keyPath: KeyPath<FileConfiguration, Value>) {
        self.keyPath = keyPath
    }
}
The comment for the wrappedValue makes it clear that it should be used for dependency injection only. And even if this property was set in an instance of an EditorViewController, it would only have an effect in the class scope.
Now, if we want to inject a mock rights value to test EditorViewController, we can do:
func testRights() {
    let editorVC = EditorViewController()
    editorVC.$rights.overrideValue = .owner
    // perform the test
}
Perfect! Thanks property wrapper!

Extending to functions

I found the same concept was interesting when applied to functions. So far, we only used it for properties. To give a real life example, let's use a NSManagedObjectContext with Core Data.
To commit the modifications the user made on the data, it's needed to call the save() function on this context. If it is possible to perform this save only when the app enters in background, I think that many developers prefer to save the context at specific points rather than relying on the app life cycle.

To be able to save the context across the app, we have to share it. And I guess you are getting it. We will use a property wrapper to do that 😏

A common way to setup a context is to use a CoreDataStack as explained in this tutorial. The goal is to have a single object responsible for setting up and holding the view context. Unfortunately, to access the stack viewContext, it is required to declare and hold a CoreDataStack in each object.
To improve that, we can have our stack declared as private in the same file as our property wrapper. Also, rather than exposing the overall NSManagedObjectContext, we will declare a protocol with the functions we want to make accessible.

final class CoreDataStack {
    // container initialisation

    var viewContext: NSManagedObjectContext { container.viewContext }
}

private var stack = CoreDataStack(modelName: "Model")

protocol Context {
    func save() throws
    func rollback()
}

extension NSManagedObjectContext: Context {}

@propertyWrapper final class ViewContext {

    var overrideValue: Context?
    var wrappedValue: { overrideValue ?? stack.viewContext }
    var projectedValue: ViewContext { self }
}

To use the context in the EditorViewController, we could do:

final class EditorViewController: UIViewController {

    @ViewContext var context

    func save() {
        do {
            try context.save()
        } catch {
            // fallback
        }
    }
}
And then to inject a mock context:
struct MockContext: Context {
    func save() throws {}
    func rollback() {}
}

// ...

editorVC.$context.overrideValue = MockContext()

Going further

In case several singletons have to be shared, an interesting solution would be to make a more generic property wrapper SingletonWrapper.

protocol Singleton {
    static var shared: Self { get }
}

@propertyWrapper final class SingletonWrapper<Root: Singleton, Value> {

    /// To be set to override the value given by the configuration
    /// when performing dependency injection
    var overrideValue: Value?
    let keyPath: KeyPath<Root, Value>

    var wrappedValue: Value {
        overrideValue ?? Root.shared[keyPath: keyPath]
    }

    var projectedValue: SingletonWrapper { self }

    init(_ keyPath: KeyPath<Root, Value>) {
        self.keyPath = keyPath
    }
}
Then for each singleton:
extension FileConfiguration: Singleton {}
typealias FileConfig<Value> = SingletonWrapper<FileConfiguration, Value>

📡

So what do you think of singletons with property wrappers? If you enjoyed reading this content, feel free to share it on Twitter. If you want to reach out to me or to know when new posts are available, you can find me on Twitter or send me an email.