Using UserDefaults in a type-safe way
Two approaches for preventing typos in keys and mismatched types — wrapping UserDefaults, and using Mirror.
- Published
This article is also published elsewhere. https://qiita.com/iganin/items/166bf2a1a4b7f94c5e01
Originally written in Japanese. This is a translation of the same piece.
Introduction
Saving data with UserDefaults is one of the common ways to persist data inside an app. Unlike storing it
in a database such as SQLite3, there is no SQL to write, and saving data is extremely easy. The problems are
that there is no type safety, and that a typo can make saving or retrieving data fail:
let value = "hoge"
UserDefaults.standard.set(value, forKey: "someValue")
// "some" is mistyped as "same", so you do not get the value you meant
// And even without the typo, the value bound to "someValue" is a String, so you cannot read it as an int
let retrievedValue = UserDefaults.standard.int(forKey: "sameValue")
This article covers what I take to be the common approach — wrapping UserDefaults — and
the Mirror-based approach proposed by Jean-David Gadina.
Environment
- macOS Sierra Version 10.12.6
- Xcode Version 9.0.0
- iOS 11.0
Wrapping UserDefaults
First, wrapping UserDefaults.
Wrapping the get and set methods as below means you do not have to specify the key at each call site, which
reduces the chance of a typo, and it guarantees the type of the UserDefaults value. It works well for one
or two properties, but doing it for many properties in a class gets fairly verbose.
public var someSettingValue: Int {
get {
return UserDefaults.standard.integer(forKey: "someSettingValue")
}
set(value) {
UserDefaults.standard.set(value, forKey: "someSettingValue")
}
}
The Mirror approach
Now for the Mirror approach. I will describe Mirror first, then show how to use it to make UserDefaults
type-safe and minimise the impact of typos.
About Mirror
Mirror is the struct that provides reflection in Swift. Writing Mirror(reflecting: object) lets you get
at and work with the type of the object or struct you assigned, the types and values of its properties, and
information about its methods.
Some of the values you can get from a Mirror:
displayStyle— tells you whether the object is a class, an enum, and so onsubjectType— the object’s typesuperclassMirror— the type of the object’s superclasschildren— the list of the object’s properties
Reference: Apple’s official documentation
public class Sample: NSObject {
public var sampleInt = 32
public var sampleString = "sample"
}
let mirror = Mirror(reflecting: Sample())
print(mirror.displayStyle)
print(mirror.subjectType)
print(mirror.superclassMirror)
mirror.children.map{ print($0) }
// Output:
Optional(class)
Sample
Optional(Mirror for NSObject)
(label: Optional("sampleInt"), value: 32)
(label: Optional("sampleString"), value: "sample")
These articles were a great help in writing this up:
Making UserDefaults type-safe with Mirror
The main event. Using the Mirror described above, we make saving and retrieving values with
UserDefaults type-safe. The broad flow is:
- Register the properties of the class you create for KVO
- Reflect value changes in
observeValue
Note: Mirror.children is used to obtain the keys in the above.
Here is the code (slightly modified from the original):
import UIKit
public class Preferences: NSObject {
// Declare @objc and dynamic so they can be used with KVO
// Note: declaring dynamic makes them usable from the Objective-C runtime
@obj dynamic var someIntegervalue: Int = 0
@obj dynamic var someStringValue: NSString = ""
@obj dynamic var someOptionalArrayValue: Array?
// Make it a singleton
static let shared = Preferences()
private override init() {
super.init()
// Use Mirror's children to get the property names.
// At initialisation, read the values from UserDefaults and,
// if they are not nil (a value exists), set them on this class's properties.
// This is the only point at which values are read from UserDefaults.
for child in Mirror(reflecting: self).children {
guard let key = child.label else { continue }
if let value = UserDefaults.standard.object(forKey: key) {
self.setValue(value, forKey: key)
}
// Register the property so value changes can be detected
addObserver(self, forKeyPath: key, options: .new, context: nil)
}
}
deinit {
// Remove the observers added in init
for child in Mirror(reflecting: self).children {
guard let key = child.label else { continue }
self.removeObserver(self, forKeyPath: key)
}
}
// Reflect value changes into UserDefaults in observeValue.
// Mirror is used here to obtain the key.
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
for child in Mirror(reflecting: self).children {
guard let key = child.label else { continue }
if (key == keyPath) {
UserDefaults.standard.set(change?[.newKey], forKey: key)
UserDefaults.standard.synchronize()
break
}
}
}
}
You create a class that gathers the properties saved to and read from UserDefaults. Mirror is used to
obtain the key both when registering for KVO and when reflecting changes in observeValue. Once the
scaffolding is in place, simply defining a property on this class is enough for it to be saved to and read
from UserDefaults. The type of the value stored in UserDefaults is guaranteed, and typos stop mattering
— because the key comes from Mirror, there is never a moment where you type the string yourself.
Thoughts
If you have a lot of properties going into UserDefaults and wrapping them makes the code unwieldy, this
might be worth considering as an option. That said, as the original article notes, this was a technique
carried over from Objective-C into Swift, so there may well be a better approach suited to Swift.