38

I used to save important App data like login credentials into UserDefaults using the following statement:

UserDefaults.standard.set("sample@email.com", forKey: "emailAddress")

Now, I have come to know SwiftUI has introduced new property wrapper called:

@AppStorage

Could anyone please explain how the new feature works?

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
Shawkath Srijon
  • 739
  • 1
  • 8
  • 17

7 Answers7

58

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:

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • Do you know if `@AppStorage` reads from disk every time the containing View struct is created? – malhal Oct 06 '20 at 15:17
  • @malhal Unfortunately I don't know, I couldn't find enough documentation for it. But I'd imagine that UserDefaults are somehow observed and AppStorage is notified to reload when changes are detected. So it might not be read from disk when a view is redrawn. But it's just a guess. – pawello2222 Oct 06 '20 at 18:26
  • Ok thanks I'll run some tests to figure it out. The reason I ask is I noticed that @FetchRequest hits the DB every time the View is created which is a big problem. – malhal Oct 06 '20 at 19:31
  • Nice answer! I've tried to **[re-implement it for using it in iOS 13 and UIKit](https://stackoverflow.com/a/64619591/5623035)**. – Mojtaba Hosseini Oct 31 '20 at 08:37
  • How can this be adapted to work with Dictionaries [:], e.g. [Int:Int]? – Nathanael Tse Jan 25 '23 at 20:21
  • From my experiments so far the variable underlying a @AppStorage declaration is read from UserDefaults only once. E.g., if you define it in at the start of a class (along with instance variables), the value only gets read from UserDefaults when the instance is instantiated. – Chris Prince Aug 27 '23 at 19:33
8

Disclaimer: iOS 14 Beta 2

In addition to the other useful answers, the types you can use in @AppStorage are (currently) limited to: Bool, Int, Double, String, URL, Data

Attempting to use other types (such as Array) results in the error: "No exact matches in call to initializer"

iOSDevil
  • 1,786
  • 3
  • 16
  • 29
  • Do you know if arrays will be added in the coming betas or do you know any workaround to save an array? –  Jul 19 '20 at 10:00
  • @Lilfaen you can encode your array into a string representation e.g. with JsonEncoder(). – Leo Oct 01 '20 at 14:18
  • @Lilfaen I added an example of how to save an `Array` in [my answer](https://stackoverflow.com/a/62563773/8697793) – pawello2222 Jan 06 '21 at 15:30
8

Re-implementation for iOS 13 and without SwiftUI

In additon to pawello2222 answer, here. is the reimplementation of the AppStorage that I named it as UserDefaultStorage:

@propertyWrapper
struct UserDefaultStorage<T: Codable> {
    private let key: String
    private let defaultValue: T

    private let userDefaults: UserDefaults

    init(key: String, default: T, store: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = `default`
        self.userDefaults = store
    }

    var wrappedValue: T {
        get {
            guard let data = userDefaults.data(forKey: key) else {
                return defaultValue
            }
            let value = try? JSONDecoder().decode(T.self, from: data)
            return value ?? defaultValue
        }
        set {
            let data = try? JSONEncoder().encode(newValue)
            userDefaults.set(data, forKey: key)
        }
    }
}

This wrapper can store/restore any kind of codable into/from the user defaults. Also, it works in iOS 13 and it doesn't need to import SwiftUI.

Usage

@UserDefaultStorage(key: "myCustomKey", default: 0)
var myValue: Int

Note that it can't be used directly as a State

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • I know you mentioned it can't currently be used as a `@State` but maybe you can make it a `DynamicProperty` somehow? So it behaves like a `@State`? (That'd be perfect). – pawello2222 Oct 31 '20 at 08:51
  • I'm working on it :) it will have some unnecessary complications I think. But I will try :) @pawello2222 – Mojtaba Hosseini Oct 31 '20 at 08:54
2

This is a persistent storage provided by SwiftUI. This code will persist the email across app launches.

struct AppStorageView: View {
    @AppStorage("emailAddress") var emailAddress = "initial@hey.com"
    var body: some View {
        TextField("Email Address", text: $emailAddress)
    }
}

With pure SwiftUI code, we can now persist such data without using UserDefaults at all.

But if you do want to access the underlying data, it is no secret that the wrapper is using UserDefaults. For example, you can still update using UserDefaults.standard.set(...), and the benefit is that AppStorage observes the store, and the SwiftUI view will update automatically.

samwize
  • 25,675
  • 15
  • 141
  • 186
1

@AppStorage property wrapper is a source of truth that allows you read data from and write it to UserDefaults. It has a value semantics. You can use @AppStorage to store any basic data type (like preferences' values or any default values) for as long as the app is installed.

For the best performance, when working with UserDefaults, you should keep the size of the stored data between 512 KB and 1 MB.

import SwiftUI

struct ContentView: View {

    @AppStorage("t_101") var t_101: String = "Terminator T-101"
    @AppStorage("t_1000", store: .standard) var t_1000 = "Liquid Metal"
  
    var body: some View {
        VStack {
            Text("Hey, \(t_101)!").foregroundColor(.indigo)
            Text("Hey, \(t_1000)!").foregroundColor(.teal)

            Divider()

            Button("Real Names") {
                t_101 = "Arnie"
                t_1000 = "Robbie"
            }
        }
    }
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
0

We can test this via simple approach:

struct Content: View {
    
    private enum Keys {
    
        static let numberOne = "myKey"
    }
    
    @AppStorage(Keys.numberOne) var keyValue2: String = "no value"

    var body: some View {
        VStack {
            Button {
                keyValue2 = "Hello"
                print(
                    UserDefaults.standard.value(forKey: Keys.numberOne) as! String
                )
            } label: {
                Text("Update")
            }
            
            Text(keyValue2)
        }
        .padding()
        .frame(width: 100)
    }
}
hbk
  • 10,908
  • 11
  • 91
  • 124
0

I have experimented a behavior that I want to share with you. I had a View (called MainView) that used an @AppStorage Bool to display or not a message to users. In this View, I also had many views displaying data loaded from a JSON (cities).

I've developed a 'Add to favorite' feature that uses UserDefault to store a list of cities ID added by user in his/her favorites. The strange behavior was this one: As soon as user added or removed a city as favorite, all MainView's body (and all its child views) was updated. That means all content was reloaded for no reason.

After hours of investigations, specially refactoring all my ObservableObjects (models), my @Published variables and so on, I've find out what was creating this mass update: The @AppStorage in my MainView! As soon as you update any key in 'UserDefaults.standard' (in my case by storing my new favorite), all views's body using an @AppStorage are updated.

So @AppStorage is awesome and easy to use. But be aware of that behavior. You don't expect that a view is updated when you set a key that is not even used in that view!