Observable settings using userDefaults

UserDefaults is fully KVO-compliant, which allows you to observe changes in the settings and react to them. However, since UserDefaults is not a class that you created yourself it might be confusing how to setup KVO. I will show you one way of setting up KVO with UserDefaults that also makes UserDefaults more convenient and guards against typos in your keys.

UserDefaults is a convenient way of storing settings in iOS. It allows you to save small amounts of data for settings in your application, but it is not suitable for large data such as images or security sensitive data such as passwords.

Example application

For this post we use a very simple single view application as an example. This application shows a settings page for controlling sound effects and music and has a controller that reacts to the changes in the settings. Building an entire applications is out of the scope of this post, so the controller just print the changes to the console instead of actually controlling music and sound effects. The settings in UserDefaults are two booleans for switching background music and  sound effects on or off and two float values (with a value between 0  and 100) to store the volume of the music and sound effects. To store the settings we use the following keys: music, sounds, musicVolume, soundsVolume.

Note: You can access the complete code for this blog on my BitBucket account in the userdefaultsdemo repository.

Defining your keys

As the first step we will start with defining the keys that are used in the application. For this we will create a enum in an extension to UserDefaults. We could create this enum anywhere, but by making it part of an extension of UserDefaults we conveniently put it in a namespace that makes it clear that we are dealing with the keys for the UserDefaults and we will make it easier to access them later.

extension UserDefaults {
    private enum Keys: String {
        case music, sounds, musicVolume, soundsVolume
    }
}

We defined the Keys enum as a private enum, because the keys are an implementation detail of storing the settings in UserDefaults. By conforming the Keys enum to the String raw type we get a convenient conversion between our enum values and the string keys used to store the data in UserDefaults.

Adding computed variables for our settings

So far we have only defined the keys, and we don’t have any way to access our settings yet, which is what we are going to fix next. For each of our four settings we will create a computed variable, that we store the data in UserDefaults when we set it and load the data from UserDefaults when we get it.

import Foundation

extension UserDefaults {
    private enum Keys: String {
        case music, sounds, musicVolume, soundsVolume
    }

    @objc var music: Bool {
        get {
            return bool(forKey: Keys.music.rawValue)
        }
        set {
            set(newValue, forKey: Keys.music.rawValue)
        }
    }

    @objc var musicVolume: Float {
        get {
            return clamp(float(forKey: Keys.musicVolume.rawValue))
        }
        set {
            set(clamp(newValue), forKey: Keys.musicVolume.rawValue)
        }
    }

    @objc var sounds: Bool {
        get {
            return bool(forKey: Keys.sounds.rawValue)
        }
        set {
            set(newValue, forKey: Keys.sounds.rawValue)
        }
    }

    @objc var soundsVolume: Float {
        get {
            return clamp(float(forKey: Keys.soundsVolume.rawValue))
        }
        set {
            set(clamp(newValue), forKey: Keys.soundsVolume.rawValue)
        }
    }

    private func clamp(_ value: Float, to range: ClosedRange<Float> = 0.0...100.0) -> Float {
        return min(range.upperBound, max(range.lowerBound, value))
    }
}

The pattern for each of these settings is very similar. We use the rawValue from the enum to get our keys and the UserDefaults API to store and retrieve the data. Notice however that all the computed variables are annotated with @objc. These annotations are necessary to make the settings KVO compliant. The @objc annotation indicates that the variables need to be made available to the Objective-C runtime.

Note: Normally you would also mark a variable as dynamic to be KVO compliant. However, since these are computed variables this is not necessary.

Reading and setting values

Now that we have setup our settings it is time to use them. There will be two places where we need to use these settings. Let’s first have a look at our ViewController where we can change the value of these properties.

import UIKit

class ViewController: UIViewController {

    @IBOutlet private var musicSwitch: UISwitch!
    @IBOutlet private var musicVolume: UISlider!
    @IBOutlet private var soundsSwitch: UISwitch!
    @IBOutlet private var soundsVolume: UISlider!
    private let musicController = MusicController()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        musicSwitch.isOn = UserDefaults.standard.music
        soundsSwitch.isOn = UserDefaults.standard.sounds
        musicVolume.value = UserDefaults.standard.musicVolume
        soundsVolume.value = UserDefaults.standard.soundsVolume
    }

    @IBAction func toggleMusic(_ sender: UISwitch) {
        UserDefaults.standard.music = sender.isOn
    }
    @IBAction func updateMusicVolume(_ sender: UISlider) {
        UserDefaults.standard.musicVolume = sender.value
    }
    @IBAction func toggleSounds(_ sender: UISwitch) {
        UserDefaults.standard.sounds = sender.isOn
    }
    @IBAction func updateSoundsVolume(_ sender: UISlider) {
        UserDefaults.standard.soundsVolume = sender.value
    }
}

In the viewWillAppear(_:) method you can see that accessing the current value is really easy, you just use the computed variable on UserDefault.standard. Setting a value is easy as well, as you can see. We just set the value of the computed variable on UserDefault.standard.

Observing values

In the ViewController we created a MusicController to play the music and sound effects, for this tutorial it doesn’t really play music, but only observes changes in the UserDefaults. The controller need to be notified of changes in the volume and when the sound effects or music are switched off using KVO.

import Foundation

class MusicController {

    private let musicObserver: NSKeyValueObservation
    private let musicVolumeObserver: NSKeyValueObservation
    private let soundsObserver: NSKeyValueObservation
    private let soundsVolumeObserver: NSKeyValueObservation
    
    init() {
        musicObserver = UserDefaults.standard.observe(\.music, options: [.new]) { (defaults, change) in
            guard let newValue = change.newValue else { return }
            print("Music switched \(newValue ? "on" : "off").")
        }
        musicVolumeObserver = UserDefaults.standard.observe(\.musicVolume, options: [.new]) { (defaults, change) in
            guard let newValue = change.newValue else { return }
            print("Music volume changed to \(newValue).")
        }
        soundsObserver = UserDefaults.standard.observe(\.sounds, options: [.new]) { (defaults, change) in
            guard let newValue = change.newValue else { return }
            print("Sounds effects switched \(newValue ? "on" : "off").")
        }
        soundsVolumeObserver = UserDefaults.standard.observe(\.soundsVolume, options: [.new]) { (defaults, change) in
            guard let newValue = change.newValue else { return }
            print("Sound effects volume changed to \(newValue).")
        }
    }
}

To observe the changes we create an observer for each of the settings in our initializer. We use the observe(_:, options:, changeHandler:) method on UserDefaults to register a closure that will be called whenever the value of the key changes. This method will return a NSKeyValueObservation and it is important that we keep hold of that value, because once the observation is released the system will stop observing changes to the key. Therefor we store the observations as a private instance property on the class, so that as long as the class stays in memory we will keep receiving notifications.

Conclusions

Using extensions on UserDefaults we can make our settings both easy to access and make them observable, which makes it easy to write code that can react to changes. We can completely encapsulate the access to the raw values in UserDefaults, making settings easy and type safe to work with.

Thoughts about things digital