A long time ago I wrote about NSUserDefaults being a good way to store user preferences within an app. Small amounts of data can be stored such as the current theme as well as other preferences for the app. The data is persisted so that it can be accessed when the app reloads.
Typically you would want to store small amounts of data in there. For larger amounts of data you should look at other means of storage. Data types such as strings, bools, numbers, dates and so on can be stored.
When Swift launched, UserDefaults was became available and enhanced NSUserDefaults by adding slightly better type safety.
With Objective-C, you could user NSUserDefaults as so:
[[NSUserDefaults standardUserDefaults] setObject:@"Dark" forKey:@"theme"];
NSString *theme = [[NSUserDefaults standardUserDefaults] objectForKey:@"theme"];
In the example above, we specify the key as “theme” and put in the string “Dark”.
When reading it (second line) we tell it the result is a string.
But, if at some point you enter this for the first line:
[[NSUserDefaults standardUserDefaults] setInteger:42 forKey:@"theme"];
If you try read this user default back as a string, you’re going to get problems and cause the app to crash.
UserDefaults added better type safety
When UserDefaults was introduced it added more type safety:
let dict: [String: Any] = ["mode": "dark"]
UserDefaults.standard.set(dict, forKey: "theme")
let themeFail = UserDefaults.standard.string(forKey: "theme") ?? "Light"
print(themeFail)
The reason this is safer is that if a dictionary was stored for the value for theme, it would accept it, but when you ask for the string back from user defaults, it will get a nil and fall back to “Light” in this instance.
Although better, it potentially does open up problems in the example shown above.
The @AppStorage Property Wrapper
Property Wrappers add functionality to properties to help make a property more simple to use. In the case of the @AppStorage property wrapper, it simplifies working with UserDefaults by removing the need to manually read and write to UserDefaults. It also allows the UI to be changed whenever its value changes working with SwiftUI’s state-driven design.
struct ContentView: View {
@AppStorage("theme") private var theme: String = "Light"
var body: some View {
VStack {
Text("Current theme: \(theme)")
Button("Switch Theme") {
theme = theme == "Light" ? "Dark" : "Light"
}
}
}
}
Here is how we use @AppStorage in SwiftUI. It is declared with the key, in the example above, this is “theme”. We then call it theme, and specify that its a String.
On line 6 we use string interpolation to add the current stored string in UserDefaults.
On lines 7 and 8 we declare a button and then use the ternary conditional operator to check the value stored in theme. If it’s light, it switches to dark and vice-versa.
If we try store a different value type in theme, we will get an error such as “Cannot assign value of type ‘Int’ to type ‘String'”, thus making it type safe.
There is one caveat in that if we decide to create another view and declare a property wrapped with @AppStorage with the same name, but store it with in Integer, this would cause the app to not function as intended, although it will not crash. If a string is stored in one view, then the other view that declared it as Int will return a 0.
You can avoid this issue by checking that you do not use mixed types for the same key.
@AppStorage makes the process of dealing with UserDefaults a little simpler and a little safer. It also adds in the ability to update the UI automatically when a value changes.
If you have any questions, please comment below.
Leave a Reply
You must be logged in to post a comment.