SwiftUI: What is @AppStorage property wrapper
AppStorage
@AppStorage
is a convenient way to save and read variables from UserDefaults and use them in the same way as @State
properties. It can be seen as a @State
property which is automatically saved to (and read from) UserDefaults
.
You can think of the following:
@AppStorage("emailAddress") var emailAddress: String = "sample@email.com"
as an equivalent of this (which is not allowed in SwiftUI and will not compile):
@State var emailAddress: String = "sample@email.com" {
get {
UserDefaults.standard.string(forKey: "emailAddress")
}
set {
UserDefaults.standard.set(newValue, forKey: "emailAddress")
}
}
Note that @AppStorage
behaves like a @State
: a change to its value will invalidate and redraw a View.
By default @AppStorage
will use UserDefaults.standard
. However, you can specify your own UserDefaults
store:
@AppStorage("emailAddress", store: UserDefaults(...)) ...
Unsupported types (e.g., Array
):
As mentioned in iOSDevil's answer, AppStorage
is currently of limited use:
types you can use in @AppStorage are (currently) limited to: Bool, Int, Double, String, URL, Data
If you want to use any other type (like Array
), you can add conformance to RawRepresentable
:
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
Demo:
struct ContentView: View {
@AppStorage("itemsInt") var itemsInt = [1, 2, 3]
@AppStorage("itemsBool") var itemsBool = [true, false, true]
var body: some View {
VStack {
Text("itemsInt: \(String(describing: itemsInt))")
Text("itemsBool: \(String(describing: itemsBool))")
Button("Add item") {
itemsInt.append(Int.random(in: 1...10))
itemsBool.append(Int.random(in: 1...10).isMultiple(of: 2))
}
}
}
}
Useful links:
- What is the @AppStorage property wrapper?
- AppStorage Property Wrapper SwiftUI
@AppStorage property wrapper prevents from dismissing views
NavigationView
can only push one detail screen unless you set .isDetailLink(false)
on the NavigationLink
.
FYI we don't use view model objects in SwiftUI, you have to learn to use the View struct correctly along with @State
, @Binding
, @FetchRequest
etc. that make the safe and efficient struct behave like an object. If you ignore this and use an object you'll experience the bugs that Swift with its value types was designed to prevent. For more info see this answer MVVM has no place in SwiftUI.
How can I listen to changes in a @AppStorage property when not in a view?
I wrote this property wrapper:
/// Property wrapper that acts the same as @AppStorage, but also provides a ``Publisher`` so that non-View types
/// can receive value updates.
@propertyWrapper
struct PublishingAppStorage<Value> {
var wrappedValue: Value {
get { storage.wrappedValue }
set {
storage.wrappedValue = newValue
subject.send(storage.wrappedValue)
}
}
var projectedValue: Self {
self
}
/// Provides access to ``AppStorage.projectedValue`` for binding purposes.
var binding: Binding<Value> {
storage.projectedValue
}
/// Provides a ``Publisher`` for non view code to respond to value updates.
private let subject = PassthroughSubject<Value, Never>()
var publisher: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
private var storage: AppStorage<Value>
init(wrappedValue: Value, _ key: String) where Value == String {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value: RawRepresentable, Value.RawValue == Int {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Data {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Int {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value: RawRepresentable, Value.RawValue == String {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == URL {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Double {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Bool {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
mutating func update() {
storage.update()
}
}
Basically it wraps @AppStorage
and adds a Publisher
. Using it is exactly the same from a declaration point of view:
@PublishedAppStorage("myValue") var myValue = 0
and accessing the value is exactly the same, however accessing the binding is slightly different as the projected value projects Self
so it done through $myValue.binding
instead of just $myValue
.
And of course now my non-view can access a publisher like this:
cancellable = settings.$myValue.publisher.sink {
print("object changed")
self.title = "Hello \($0)"
}
SwiftUI: Why does @AppStorage work differently to my custom binding?
It does not work even when running app, because of that "." in key name (looks like this is AppStorage limitation, so use simple notation, like
isSheet
.IMO the test-case is not correct, because it overrides defaults by arguments domain, but it is read-only, so writing is tried into persistent domain, but there might be same value there, so no change (read
didSet
) event is generated, so there no update in UI.To test this it should be paired events inside app, ie. one gives
AppStorage
value true, other one gives false
*Note: boolean value is presented in arguments as 0/1 (not XML), lie -isSheet 1
AppStorage vs CoreData
AppStorage
is a property-wrapper around UserDefaults
for SwiftUI
. So whatever you're storing using AppStorage
is available also via UserDefaults
.
@AppStorage("isDarkMode") private var isDarkMode: Bool = false
//...
let isDarkMode = UserDefaults.standard.bool(forKey: "isDarkMode")
CoreData
is where you store large amounts of data. You can go through this post.
Mockable @AppStorage in SwiftUI
The AppStorage
allows to specify storage to be used (by default standard UserDefaults
), so this feature can be utilised for your purpose.
One of possible approaches is to subclass standard user defaults, and mock it later if/where needed.
class MyUserDefaults: UserDefaults {
// override/add anything needed here
}
struct DemoView: View {
@AppStorage("numberOfHandsPlayed", store: MyUserDefaults()) var numberOfHandsPlayed: Int = 3
// ... other code
Related Topics
Calculating Bearing Between Two Cllocation Points in Swift
How Much Delay of iOS Push Notification
Xctest and Asynchronous Testing in Xcode 6
How to Add the Wechat API to a Swift Project
Ios: Detect If the Device Is iPhone X Family (Frameless)
Record Audio and Save Permanently in iOS
Change Button Background Color Using Swift Language
App Icons Not Included in Build from Xcode
Prevent Uialertcontroller to Dismiss
What Does the Text Inside Parentheses in @Interface and @Implementation Directives Mean
How to Convert a Nib-Based Project to a Storyboard-Based
Error: Ld: Library Not Found for -Lpods with Cocoapods
(Swift) Storing and Retrieving Array to Nsuserdefaults
Amazon Aws iOS Sdk: How to List All File Names in a Folder
iOS - How to Play a Video with Transparency
First App Update, User Data Lost (Was Stored in Documents Directory)