Swiftui: What Is @Appstorage Property Wrapper

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?


  1. 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.

  2. 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.

  3. 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



Leave a reply



Submit