Avatar

Let’s Simplify the Work with UserDefaults

Everyone has worked with UserDefaults in order to store some simple data and knows that working with that storage is easy as it can be. But today I’m going to improve the interaction with it a bit though! Let’s start with the most obvious solution and implement something new and elegant. 😌

Imagine we have some service — SettingsService. This service knows about the app’s settings — which theme is used (dark, light), whether notifications are enabled and so on. To implement it the majority of developers will think about the UserDefaults at first. Of course, it depends on the case, but let’s simplify it. Our first simplest solution:

1. Our first simplest solution:

class SettingsService {

    private enum Keys {
        static let isNotificationsEnabled = "isNotificationsEnabled"
    }

    var isNotificationsEnabled: Bool {
        get {
            let isEnabled = UserDefaults.standard.value(forKey: Keys.isNotificationsEnabled) as? Bool
            return isEnabled ?? true
        }
        set {
            UserDefaults.standard.setValue(newValue, forKey: Keys.isNotificationsEnabled)
        }
    }
}

For simplifying, I explicitly use UserDefaults.standard but in a real project you’d better to store it on a property and use DI of course.

2. The next step what I want to take is to get rid of Keys enum — use the #function instead:

class SettingsService {

    var isNotificationsEnabled: Bool {
        get {
            let isEnabled = UserDefaults.standard.value(forKey: #function) as? Bool
            return isEnabled ?? true
        }
        set {
            UserDefaults.standard.setValue(newValue, forKey: #function)
        }
    }
}

Looks better! Let’s go further :)

3. Subscript time! We’ve just wrapped the value(forKey:) function into a subscript with a generic:

extension UserDefaults {

    subscript<T>(key: String) -> T? {
        get {
            return value(forKey: key) as? T
        }
        set {
            set(newValue, forKey: key)
        }
    }
}

class SettingsService {

    var isNotificationsEnabled: Bool {
        get {
            return UserDefaults.standard[#function] ?? true
        }
        set {
            UserDefaults.standard[#function] = newValue
        }
    }
}

It already looks pretty neat! But what about Enums? 🤔

enum AppTheme: Int {
    case light
    case dark
}

class SettingsService {

    var appTheme: AppTheme {
        get {
            if let rawValue: AppTheme.RawValue = UserDefaults.standard[#function], let theme = AppTheme(rawValue: rawValue) {
                return theme
            }
            return .light
        }
        set {
            UserDefaults.standard[#function] = newValue.rawValue
        }
    }
}

There’s a place for refactoring!

4. Let’s write a similar subscript only for RawRepresentable values:

extension UserDefaults {
    
    subscript<T: RawRepresentable>(key: String) -> T? {
        get {
            if let rawValue = value(forKey: key) as? T.RawValue {
                return T(rawValue: rawValue)
            }
            return nil
        }
        set {
            set(newValue?.rawValue, forKey: key)
        }
    }
}

class SettingsService {
    
    var appTheme: AppTheme {
        get {
            return UserDefaults.standard[#function] ?? .light
        }
        set {
            UserDefaults.standard[#function] = newValue
        }
    }
}

Ready for production! Please, be aware that this extension is only applied for enums with RawRepresentable presentation.


I hope you enjoyed my extensions! If you know any ways of improving it — let me know! There’s a final extension on UserDefaults. Feel free to test it out! :)

@readaggregator