How to create a Custom Preferences Manager with Swift

José Gutiérrez
4 min readJan 14, 2021

--

In most iOS projects we need to save users preferences such as preferred display mode, media playback speed, high-scores or even some privacy settings, sure, we can always entrust this task to the back end team and retrieve the data through a service, but this would be an overwork for both back and front end if we are talking about simple, small size and not sensitive data, in this case, it would be better for everybody to save it locally in the device.

Fortunately, Swift provides an Interface that allows us to do this very easily, from the official Apple documentation:

“The NSUserDefaults class provides a programmatic interface for interacting with the defaults system. … At runtime, you use NSUserDefaults objects to read the defaults that your app uses from a user’s defaults database.”

So… how to use the NSUserDefaults class?

First, we need to create an instance of NSUserDefaults

let userDefaults = UserDefaults.standard

With this instance we can write and read data for any specific key we define.

Writing NSUserDefaults

It is as simple as assigning a value for a key, for example

userDefaults.set("horizontal", forKey: "displayMode")

Reading NSUserDefaults

if let displayMode = defaults.stringForKey("displayMode") {
print(displayMode)
}

This would print “horizontal” into the console, but what happens if a developer has a typo and writes “displaysMode” instead of “displayMode” in the snippet above? now our variable displayMode would be nil and we couldn't get the stored preference.

In order to prevent these errors as the project becomes more complex and we have to access the users preferences from different classes, we can create a custom preferences manager that allows us to read and write NSUserDefaults from different places in our project without typos.

Creating a Custom Preferences Manager

We will create a protocol with a single rawValue property of type String that will have to implement every enum that we use to store the keys of our preferences.

protocol Preference {
var rawValue: String { get }
}

Now, we create another protocol with the functions that allows us to write, read and delete data from NSUserDefaults

protocol PreferencesProtocol {
associatedtype Enumeration: Preference
static func set<T>(_ value: T, forKey key: Enumeration)
static func get<T>(_ value: T.Type, forKey key: Enumeration) -> T?
static func removeValue(forKey key: Enumeration)
}

We use associatedtype that lets us make the protocol generic, in essence, it marks holes in protocols that must be filled by whatever types conform to those protocols. And we make our associatedtype Enumeration conform to the protocol Preference, this way we can be sure that all the keys we use will have a property rawValue of type String. Also, we use generic parameters so we don’t have to create set/get functions for every type of data we want to store (If you want another post explaining how to use associatedtype and Generics let me know in the comments).

Note: we can use NSUserDefaults to store any type of data that conforms to NSData, NSString, NSNumber, NSDate, NSArray or NSDictionary.

Lastly, we have to provide default implementations for the PreferencesProtocol functions, note that we have to create a new instance of userDefaults in every function we use NSUserDefaults.

These functions can be overriden by any class that conforms to this protocol if required.

extension PreferencesProtocol {
static func set<T>(_ value: T, forKey key: Enumeration) {
let userDefaults = UserDefaults.standard
userDefaults.set(value, forKey: key.rawValue)
}
static func get<T>(_ value: T.Type, forKey key: Enumeration) -> T? {
let userDefaults = UserDefaults.standard
return userDefaults.object(forKey: key.rawValue) as? T
}
static func removeValue(forKey key: Enumeration) {
let userDefaults = UserDefaults.standard
userDefaults.removeObject(forKey: key.rawValue)
}
}

How to use it

Once we have created all the protocols and implemented the functions we need, is time to use them.

Now we can create a class in charge of managing the user preferences, for example, MediaPreferencesManager. First, we have to create an enum of type String and conform to the Preference protocol for the keys we will use.

enum MediaPreferences: String, Preference {
case framesPerSecond = "frames_per_second"
case displayMode
case playbackSpeed
}

Note that you can change the rawValue property for every key just by assigning it.

Finally, we can create the MediaPreferencesManager class that conforms to PreferencesProtocol. Use typealias and make Enumeration = MediaPreferences so Swift knows that we will use this enum to set the keys of our preferences.

class MediaPreferencesManager: PreferencesProtocol {
typealias Enumeration = MediaPreferences
}

And that’s it, now you can use the MediaPreferencesManager as simple as follows

//Writing data
MediaPreferencesManager.set(60, forKey: .framesPerSecond)
//Reading data
if let fps = MediaPreferencesManager.get(Int.self, forKey: .framesPerSecond) {
print(fps)
}
//Removing data
MediaPreferencesManager.removeValue(forKey: .framesPerSecond)
if let fps = MediaPreferencesManager.get(Int.self, forKey: .framesPerSecond) {
print(fps)
} else {
print("preference removed")
}

This will print ‘60’, ‘preference removed’ to the console.

Note that this approach avoids typos and allows to create different Preferences Managers according to the type of preferences we are storing instead of handling all the user’s preferences in just one place, for example, we can create an AudioPreferencesManager, VideoPreferencesManager, SecurityPreferencesManager, PrivacyPreferencesManager, etc.

I hope you guys find this post useful and thanks for turning in! Happy coding!

Co-author: Luis Cabarique

--

--