Type-Safe UserDefaults API
As the Swift APIs have evolved, Apple has also been improving their own frameworks to make them easier to work with in Swift. Two API improvements I’ve personally appreciated are how we work with NSAttributedString
keys, and Notification
names.
Deriving inspiration from those additions, I thought UserDefaults
could probably benefit from a similar approach.
Keys
Lets start by defining a UserDefaults.Key
type similar to NSAttributedString.Key
.
extension UserDefaults {
public struct Key: Hashable, RawRepresentable, ExpressibleByStringLiteral {
public var rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
public init(stringLiteral value: String) {
self.rawValue = value
}
}
}
Notice we’re adding ExpressibleByStringLiteral
support to allow us to simplify key definitions.
let foo: UserDefaults.Key = "foo"
Type-Safety
Now lets add API that allows us to use our new Key
type instead of raw strings. While we’re at it lets also add type-safety to allow us to infer function calls.
Note: This is a lighweight approach that doesn’t provide key-to-value type-safety. You will still need to know the expected type that’s returned for a given key.
func set<T>(_ value: T?, forKey key: Key) {
set(value, forKey: key.rawValue)
}
func value<T>(forKey key: Key) -> T? {
return value(forKey: key.rawValue) as? T
}
While we’re at it, let’s add a subscript too.
subscript<T>(key: Key) -> T? {
get { return value(forKey: key) }
set { set(newValue, forKey: key.rawValue) }
}
Finally we need to an API we can use when registering our initial values.
func register(defaults: [Key: Any]) {
let mapped = Dictionary(uniqueKeysWithValues: defaults
.map { ($0.rawValue, $1) } // ::hint:: tuple key/value
)
register(defaults: mapped)
}
API Usage
Now we have a clean API let define some keys:
public extension UserDefaults.Key {
static let showLineNumbers: UserDefaults.Key = "ShowLineNumbers"
static let fontFamily: UserDefaults.Key = "SourceFontFamily"
static let fontSize: UserDefaults.Key = "SourceFontSize"
static let defaultFontSize: UserDefaults.Key = "DefaultSourceFontSize"
}
Now we can make use of these keys and define some initial values:
let defaults = UserDefaults.standard
defaults.register(defaults: [
.showLineNumbers: false,
.fontFamily: defaultFont.familyName!,
.fontSize: defaultSize,
.defaultFontSize: defaultSize
])
showNumbers = defaults[.showLineNumbers]
defaults[.showLineNumbers] = false
Conclusion
I think this is a clean solution that feels familiar to other iOS framework APIs and is simple enough to start adding to an existing project immediately with no overhead.
There are other approaches to this problem, but I found this to be a lightweight solution that adds value from the outset, without incurring additional complexity in your code.
Checkout the link to get the full source, including more overrides and conveniences for dealing with various types.