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 : )