UserDefaults and Property Wrappers in Swift
Andres Rojas
Posted on April 6, 2020
The easiest way to save your objects into UserDefaults
According to Apple's documentation,
UserDefaults is an interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app.
The UserDefaults class provides convenience methods for accessing common types such as floats, doubles, integers, Boolean values, and URLs. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
Well, if we need NSData, how does it work?
First of all, we need to implement Codable, this is the easiest way to encode/decode our structs.
Let's define our Struct.
struct Task: Codable {
var title: String
var isDone: Bool
}
Before start using PropertyWrappers I'll show you use UserDefaults to save and get our objects.
func saveTask(_ task: Task, forKey key: String) throws {
let userDefaults = UserDefaults.standard
let encoder = JSONEncoder()
do {
let data = try encoder.encode(task)
userDefaults.set(data, forKey: key)
userDefaults.synchronize()
} catch {
throw(Errors.encodeError)
}
}
func getTask(forKey key: String) throws -> Task? {
let userDefaults = UserDefaults.standard
let decoder = JSONDecoder()
guard let data = userDefaults.object(forKey: key) as? Data else {
throw(Errors.noUserDefaults)
}
do {
let task = try decoder.decode(Task.self, from: data)
return task
} catch {
throw(Errors.decodeError)
}
}
As you can see we have declared two different functions:
- save Task, which encodes our Task model and use UserDefaults to save the whole data.
- getTask, which uses UserDafaults to get the data and decodes it to get the Task model.
This is pretty simple to use, but the problem comes when we need to get and save our objects several times, so we need to be aware to call every function at a specific time.
What can we do to improve this process? Property Wrappers to the rescue!
A property wrapper is a swift feature that allows us to define a custom type that implements get and set methods, and we can reuse it everywhere in the app.
Before starting implementing our property wrapper, there are two important things to take into account:
- Be sure you are using the
@propertyWreapper
annotation - You need to use a property called
wrappedValue
This property will implement the get and set methods and here is where we're going to add our code to use the UserDefaults.
@propertyWrapper
struct UserDefaultsWrapper<Value: Codable> {
let key: String
let defaultValue: Value
let userDefaults = UserDefaults.standard
var wrappedValue: Value {
get {
let data = userDefaults.data(forKey: key)
let value = data.flatMap { try? JSONDecoder().decode(Value.self, from: $0) }
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
userDefaults.set(data, forKey: key)
userDefaults.synchronize()
}
}
}
This property wrapper expects two parameters: key and defaultValue, we use the default value when the UserDefaults can't retrieve the data, this will happen the first time when we try to access the property.
Finally, the implementation looks like this:
let defaultTasks: [Task] = [
Task(title: "Learn SwiftUI", isDone: false),
Task(title: "Share this article", isDone: true)
]
struct TaskProvider {
@UserDefaultsWrapper(key: "Tasks", defaultValue: defaultTasks)
var tasks: [Task]
}
print(TaskProvider().tasks)
And the result will be:
2 elements
▿ __lldb_expr_4.Task
- title: "Learn SwiftUI"
- isDone: false
▿ __lldb_expr_4.Task
- title: "Share this article"
- isDone: true
If you want to check the full implementation please visit this Gist
Posted on April 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.