UserDefaults value updating SwiftUI View
@available(iOS 14.0, OSX 10.16, tvOS 14.0, watchOS 7.0, *)
There is common use case when the app need to update View when some value stored from UserDefaults storage has been changed. @AppStorage property wrapper is for the rescue.
Minimum implementation example can be:
import SwiftUI
struct ContentView: View {
@AppStorage("logged_in") var isLoggedIn = false
var body: some View {
Toggle("Is user logged in", isOn: $isLoggedIn)
}
}Few things to notice here:
- Initializer of property wrapper accepts
Stringvalue which represents thekeyfor storing. - Type of the storing value is infered by the compiler automatically so you can omit generic parameter
@AppStorage<Bool>("logged_in") var isLoggedIn = false - Created property is of type Bool
var isLoggedIn: Bool { get nonmutating set }. In fact property wrapper exposesprojectedValuevariable of typeBinding<Bool>. That meansisLoggedIncan be used with$sign for accessing it’sBinding. That is exactly whatToggleneeds as one of the arguments. Whenever user taps on the Toggle, it will switchisLoggedInvariable. Framework will write new value using the appropriateUserDefaultskey. And that will trigger current view update to reflect change. But that is the exact same behaviour forToggleeven without using fancy property wrapper - it toggles automatically when user tap on it. In order to illustrate bindinb even more clearly, let’s add one more control.
import SwiftUI
struct ContentView: View {
@AppStorage("logged_in") var isLoggedIn = false
var body: some View {
VStack {
Toggle("Is user logged in", isOn: $isLoggedIn)
Text(isLoggedIn ? "Logged in" : "Logged out")
}
}
}This time since Text view is declaring to render different text depending on isLoggedIn variable, view is recreating each time that variable changes.
Another way to declare variable backed by UserDefaults is to specify initial value via initializer of property wrapper itself:
@AppStorage(wrappedValue: false, "logged_in") var isLoggedInWhich one to use is only matter of personal taste. But what is worse to mention is the fact that actual writing of value into the disk only happens with mutating of property wrapper value only after initialization. Before that view will use wrappedValue from the memory but not from the actual disk storage. You should be aware of that especially if other parts of the application uses “old way” to access UserDefaults values like so: UserDefaults.standard.value(forKey:). Consider such example:
import SwiftUI
struct ContentView: View {
@AppStorage("logged_in") var isLoggedIn = false
var body: some View {
VStack {
Toggle("Is user logged in", isOn: $isLoggedIn)
Text(isLoggedIn ? "Logged in" : "Logged out")
Button("Get value old way") {
let isLoggedIn = UserDefaults.standard.value(forKey: "logged_in") as? Bool
print(isLoggedIn)
}
}
}
}If user has never tap on Toggle, tap on the Button will always print “nil” despite isLoggedIn will be false. Also it’s easy to check by exploring .plist file of UserDefaults store. It’s located under “~Documents/Library/Preferences” directory of the Simulator. You can check path by printing out NSHomeDirectory() somewhere in the app life cycle.
Although declaration of wrapper is generic @frozen @propertyWrapper public struct AppStorage<Value> : DynamicProperty, all available initializers has some constraint. It means you can’t use @AppStorage property wrapper for any type you want.
Out of the box AppStorage works with such types: Bool, Int, Double, String, URL, Data and RawRepresentable. If you will try to use this property wrapper for other type, compiler will complain with error message No exact matches in call to initializer.
Interesting case here is RawRepresentable. It’s allowed to use Enums for storing values to the UserDefaults store, but Enum need to satisfy RawRepresentable protocol while RawValue should be either String or Int. Here is an example:
import SwiftUI
struct ContentView: View {
enum UserType: String {
case admin
case member
case guest
}
@AppStorage("user_role") var userType = UserType.member
var body: some View {
VStack {
Text(userType.rawValue)
Button("Make admin") {
userType = .admin
}
}
}
}With that being said, it can be not only Enum but a Struct, for example. Just need to implement protocol:
import SwiftUI
struct ContentView: View {
@AppStorage("user_role") var userType = TopUser(rawValue: 12)
var body: some View {
VStack {
Text(String(userType.rawValue))
Button("Make admin") {
userType = TopUser(rawValue: 42)
}
}
}
}
struct TopUser: RawRepresentable {
var rawValue: Int
}