Swift Utilities — Simplifying Work with UserDefaults

Vyacheslav Ansimov
3 min readNov 19, 2023

--

Over the years as an iOS developer, I’ve accumulated a variety of tools and helpful items that make the development process easier. In this article, I want to share one of those tools. This will not be a long article. I’ll show how to use this utility and demonstrate it in action. I hope the article will be useful for you.

Working with UserDefaults can be convenient and simple, thanks to the UserDefaultsWrapper. Let’s explore how it works. UserDefaultsWrapper is a property wrapper that makes it easier to save and retrieve data from

UserDefaults. It is particularly useful when you work with data types such as URL, arrays, dictionaries, primitive types (String, Int, Bool, etc.).

import Foundation

/// - Parameters:
/// - key: Data will be saved and extracted using this key
/// - defaultValue: This value will be substituted when there is no value
/// - userDefaults: NSUserDefaults is a hierarchical persistent interprocess (optionally distributed) key-value store, optimized for storing user settings.
///
/// Types an UserDefaultsWrapper can work with:
///```
/// URL?
/// [Any]?
/// [String : Any]?
/// String?
/// [String]?
/// Data?
/// Bool
/// Int
/// Float
/// Double
/// ```
///
/// - Warning:
/// If the type is not optional, default == nil will cause a crash.
///
/// Exemple:
/// ```
/// class User {
///
/// @UserDefaultsWrapper<String>(key: "FirstName", default: "John") var firstName
/// @UserDefaultsWrapper(key: "LastName", default: "John") var lastName: String
///
/// // OR
///
/// @UserDefaultsWrapper(key: "Age") var age: Int?
/// @UserDefaultsWrapper(key: "Old", default: 6) var old: Int?
/// }
///
/// let user = User()
/// print(user.firstName) // John
/// user.firstName = "Misha"
/// print(user.firstName) // Misha
/// $user.firstName.removeObject()
///
/// print(user.old) // optional(6)
/// user.old = 18
/// print(user.old) // optional(18)
/// user.old = nil
/// print(user.old) // optional(6)
///
/// print(user.age) // nil
/// user.age = 18
/// print(user.age) // optional(18)
/// user.age = nil
/// print(user.age) // nil
/// ```
@propertyWrapper
public struct UserDefaultsWrapper<T> {
private let key: String
private let defaultValue: T!
private let userDefaults: UserDefaults

public var wrappedValue: T {
get {
let anyValue = userDefaults.value(forKey: key)
let value: T = (anyValue as? T) ?? defaultValue
return value
}
set {
if let optional = newValue as? AnyOptional, optional.isNil {
userDefaults.removeObject(forKey: key)

if let defaultValue = defaultValue {
self.set(newValue: defaultValue)
}
} else {
self.set(newValue: newValue)
}
userDefaults.synchronize()
}
}

public var projectedValue: ActionUserDefault { self }

/// - Parameters:
/// - key: Data will be saved and extracted using this key
/// - defaultValue: This value will be substituted when there is no value
/// - userDefaults: NSUserDefaults is a hierarchical persistent interprocess (optionally distributed) key-value store, optimized for storing user settings.
///
/// - Warning:
/// If the type is not optional, default == nil will cause a crash.
public init(key: String, default defaultValue: T? = nil, userDefaults: UserDefaults = .standard) {
self.key = key
self.userDefaults = userDefaults
self.defaultValue = defaultValue
}

private func set(newValue: T) {
userDefaults.setValue(newValue, forKey: key)
}
}

// MARK: - ActionUserDefault

extension UserDefaultsWrapper: ActionUserDefault {

public func removeObject() {
userDefaults.removeObject(forKey: key)

if let defaultValue = defaultValue {
self.set(newValue: defaultValue)
}
}
}

// MARK: - Helper classes

private protocol AnyOptional {
var isNil: Bool { get }
}

extension Optional: AnyOptional {
var isNil: Bool { self == nil }
}

public protocol ActionUserDefault {
func removeObject()
}

Usage

An example of using UserDefaultsWrapper might look like this

class User {

@UserDefaultsWrapper<String>(key: "FirstName", default: "John") var firstName
@UserDefaultsWrapper(key: "LastName", default: "John") var lastName: String

@UserDefaultsWrapper(key: "Age") var age: Int?
@UserDefaultsWrapper(key: "Old", default: 6) var old: Int?
}

Here, firstName, lastName, Age, and Old are properties that automatically save their values to UserDefaults and retrieve them from there.

UserDefaultsWrapper also includes a removeObject() method, which deletes a value from UserDefaults and, if available, returns the default value.

$user.firstName.removeObject()

Advantages and Application

UserDefaultsWrapper simplifies and makes working with UserDefaults safer. It reduces repetitive code and prevents potential errors associated with using the wrong key for saving and retrieving data. By using this approach, you can improve the structure and readability of your code.

More stories

--

--