Better NSUserDefaults with Swift

After reading the excellent "Type-Safe User Default" article from Mike Ash, I thought it would be interesting to describe the solution I use in my projects, as the approach is quite different.

I came up with this solution a long time ago, and the first implementation was originally written in Objective-C.
The goal remains the same: add safety to Cocoa's NSUserDefaults.

As a reminder, NSUserDefaults is used to persist user settings in an app.
While it does the job, it has some drawbacks when it come to type safety, because values are not associated with a type. Moreover, values are retrieved and written using a string key, meaning it's very easy to shoot yourself in the foot with typos.

An easy way to add safety, for both the values and the keys, is to create a wrapper class on top of NSUserDefaults.
Something like:

public class Preferences
{
    public var someValue: Int
    {
        get
        {
            return UserDefaults.standard.integer( forKey: "someValue" )
        }
        set( value )
        {
            UserDefaults.standard.set( value, forKey: "someValue" )
        }
    }
}

Unfortunately, such an approach leads to a lot of boilerplate code, as you'll need to wrap similarly each of your user default value.

The solution I propose is to automate the wrapping code on runtime, using reflection.
This used to be done with the Objective-C runtime.
With Swift, it's even easier through the use of the Mirror class.

In Swift, you can use a Mirror to reflect the properties of a custom class, like:

import Cocoa

public class Preferences
{
    public var someValue:      Int = 0
    public var someOtherValue: Int = 42
}

let p = Preferences()
let m = Mirror( reflecting: p )

for c in m.children
{
    print( ( c.label ?? "" ) + " => " + String( describing: c.value ) )
}

Here, the for loop will enumerate each property of the Preferences class, printing its name and actual value.

Now in order to avoid wrapping our properties, we'll use Key Value Observing (KVO).
This will allow us to be notified of any change of a property.

We'll start by reading the actual property value from NSUserDefaults.
Then we'll add an observer for each property, using a Mirror.
We'll do this in the class' initialiser:

override init()
{
    super.init()

    for c in Mirror( reflecting: self ).children
    {
        guard let key = c.label else
        {
            continue
        }
        
        self.setValue( UserDefaults.standard.object( forKey: key ), forKey: key )
        self.addObserver( self, forKeyPath: key, options: .new, context: nil )
    }
}

We'll also need to remove the observers in deinit:

deinit
{
    for c in Mirror( reflecting: self ).children
    {
        guard let key = c.label else
        {
            continue
        }
    
        self.removeObserver( self, forKeyPath: key )
    }
}

That's it. From now on, we'll receive KVO notifications when the value of a property is changed.
We simply need to handle these notifications, and write the values to NSUserDefaults:

public override func observeValue( forKeyPath keyPath: String?, of object: Any?, change: [ NSKeyValueChangeKey : Any ]?, context: UnsafeMutableRawPointer? )
{
    var found = false 

    for c in Mirror( reflecting: self ).children
    {
        guard let key = c.label else
        {
            continue
        }
    
        if( key == keyPath )
        {
            UserDefaults.standard.set( change?[ NSKeyValueChangeKey.newKey ], forKey: key )
        
            found = true
        
            break
        }
    }

    if( found == false )
    {
        super.observeValue( forKeyPath: keyPath, of: object, change: change, context: context )
    }
}

With this code in place, what is left to do is to declare the properties you want:

@objc public class Preferences: NSObject
{
    @objc public dynamic var someIntegerValue:        Int       = 0
    @objc public dynamic var someStringValue:         NSString  = ""
    @objc public dynamic var someOptionalArrayValue:  NSArray?

    // Previous methods here…
}

That's it. You don't have to write anything more.
Note the @objc and dynamic keywords, that are required for KVO.

I like this approach, because it adds type-safety to NSUserDefaults while keeping the code very simple.
All you have to do is to declare the properties you want.

Here's the complete Swift class, for reference:
https://github.com/macmade/user-defaults/blob/master/swift/Preferences.swift

And for Objective-C users, here's a similar implementation, using the Objective-C runtime:
https://github.com/macmade/user-defaults/tree/master/objective-c

Enjoy : )

Comments

Author
Hm
Date
10/13/2017 05:43
That is the ugliest piece of swift code that I have ever seen.
Author
Nicolas Bouilleaud
Date
10/13/2017 11:30
Oh, that is really elegant. I wish I had seen this years ago in objc :D

If I understand correctly, the Preferences class only reads from the userdefaults once, when created?

I wonder if we could use an single “didSet” method for all the properties instead of relying on KVO (and the @objc dynamic dance)? Maybe by boxing the property types in some kind of generic proxy.

Add a comment