
This video is only available to subscribers. Start a subscription today to get access to this and 469 other videos.
Strongly Typed UserDefaults
Note: The demo in this has an error in it. You'll need to use a
rawValue
when setting the.theme
key, which means you'll have to change the type toString
. I've updated the source code to reflect this, but you can see Episode 422 for a slightly different approach.
Episode Links
We can store preferences in UserDefaults
using string keys. This will allow us to store settings and other lightweight data that will be persisted across app launches.
let theme = UserDefaults.standard.string(forKey: "theme")
return theme == "dark"
This is great, however there are 2 ways we can mess up here. If we mispell the key, we'll be reading from the wrong place (or writing to the wrong place). Additionally, the value here is a string, but there might be a number there, or a boolean, or something else.
Let's start by eliminating the first issue: string keys. Any time you are repeating the same string in multiple places you want to start thinking about defining that once and using that everywhere.
We could use a constant, but we can get some nice code completion if we leverage Swift's type system
extension UserDefaults {
struct Key {
fileprivate let name: String
init(name: String) {
self.name = name
}
}
// ...
}
This gives us a type that will wrap the string for us. We've marked the name fileprivate
so that we can read it in other methods in this file, but it will be private to outside callers.
Next we can add some methods to read, write, and remove values using this new Key
type:
func set(_ value: Any, for key: Key) {
set(value, forKey: key.name)
}
func value(for key: Key) -> Any? {
return value(forKey: key.name) as? V
}
func removeValue(for key: Key) {
removeObject(forKey: key.name)
}
With these in place we can go back to our view controller (or wherever we want to place our app's settings) and define the key:
extension UserDefaults.Key {
static let theme = UserDefaults.Key(name: "theme")
}
Usage becomes much nicer:
// get the theme
let theme: String = UserDefaults.standard.value(for: .theme)
// set the theme
UserDefaults.standard.set("dark", forKey: .theme)
This is nicer, but there's nothing to stop you from sending a URL
or a Date
here. Or even "hamburger"
. It would be nice if we could constrain the value used underneath this key...
extension UserDefaults {
struct Key<Value> {
fileprivate var name: String
init(name: String) {
self.name = name
}
}
// ...
}
Here we've just added a generic paramter to Key
. This breaks the methods we wrote so we have to specify the Value
. We can do that by making the methods themselves generic:
func set<V>(_ value: V, for key: Key<V>) {
set(value, forKey: key.name)
}
This means that whatever type V
is (which can be defined by the callsite) it will constrain our key and value to match.
The other methods can have a similar treatment:
func value<V>(for key: Key<V>) -> V? {
return value(forKey: key.name) as? V
}
func removeValue<V>(for key: Key<V>) {
removeObject(forKey: key.name)
}
Now we can change our theme
key to be constrained by the type of data it should be:
extension UserDefaults.Key where Value == String {
static let theme = UserDefaults.Key<String>(name: "theme")
}
Now it is only possible to use the intended data type when storing with these new methods.
Usage becomes a lot nicer too!
@IBAction func themeChanged(_ sender: UISwitch) {
let theme: Theme = sender.isOn ? .light : .dark
UserDefaults.standard.set(theme.rawValue, for: .theme)
updateForTheme()
}
let isDark = UserDefaults.standard.value(for: .theme) == Theme.dark.rawValue
Pretty nice!
My thanks to Daniel Tull who's blog post inspired this episode.