Using the @AppStorage Property Wrapper in SwiftUI

There are a certain things when developing iOS apps that all developers deal with sooner or later. One of those is saving and reading values to and from the user defaults file respectively.

User defaults is actually a property list file (plist) that can store values of basic datatypes. Since it's a property list file, content is stored in key-value pairs. The purpose of user defaults is to let us save bits of data and read them back quickly and effortlessly. This data is available app-wide and can be updated from anywhere inside an app. User defaults is not meant though to be used as a persistent storage for large data. On the contrary, it's best for keeping stuff like user preferences or settings. Also, it's the worst choice a developer can make in order to save sensitive data. User defaults provide no security at all, and it's easily accessible; so, just don't do that.

In programming terms now, it's known from the pre-SwiftUI days that we can access user defaults using the UserDefaults class in the Foundation framework. This is still valid nowadays as well. However, SwiftUI makes available another API to use; the @AppStorage property wrapper.

@AppStorage is one of the various property wrappers available to use in SwiftUI. We can consider it as an app-wide source of truth, which, however, has one great difference compared to others; modified values do not remain in memory, but they get written to the user defaults file. Reading and writing values is straightforward with @AppStorage, and in this post we'll go through a few concepts around this topic.

Using the @AppStorage property wrapper

Suppose that we're building an app where we have a user profile section. There, we display some bits of information that users are able to modify in a modal sheet. As an example, see the following simple demonstration:

Display and update the following user preferences: Profile name, age and online status.

Starting from the view that displays the user profile details, the first thing we need is to access the user defaults and fetch each piece of data we need. In order to do that for the profile name, we declare the following property, marked with the @AppStorage property wrapper as so:

@AppStorage("profileName", store: .standard) var profileName = "Not Set"

The first argument provided above is the key matching to the data we want to retrieve. The second argument points to the users default file that @AppStorage should use. In most cases, the default one is what we'll want to use, and we specify that passing the .standard value. However, it's also possible to provide a different user defaults file to use instead:

@AppStorage("profileName", store: UserDefaults(suiteName: "custom_user_defaults")) var profileName = "Not Set"

In any case, you'll notice that we assign an initial value to the profileName property, and doing so has a double purpose:

  1. To implicitly declare the data type of the value we are trying to read or write.
  2. To specify an initial value in case no prior value has been written to user defaults yet.

Indeed, the first time the app will run will show the "Not Set" value, but after users have updated the profile name, their input is what will appear from there on.

Besides profileName, here are two more properties marked with the @AppStorage property wrapper that complete the example demonstrated above:

@AppStorage("age", store: .standard) var age = 40
@AppStorage("status", store: .standard) var status = OnlineStatus.Visible

age is an integer value, while status is a value of a custom type called OnlineStatus; an enum with String raw values:

enum OnlineStatus: String, CaseIterable {
    case Visible, Invisible, Busy, Idle
    
    var color: Color {
        switch self {
            case .Visible: return Color.green
            case .Invisible: return Color.gray
            case .Busy: return Color.red
            case .Idle: return Color.orange
        }
    }
}

Besides String and Int, other data types that user defaults can store are:

  • Bool
  • Double
  • URL
  • Data

Handling the above properties inside the SwiftUI view, is similar to any other property marked with the @State or other property wrappers:

VStack {
    // ...
    Text(profileName)
        .fontWeight(.medium)
    // ...
    
    // ...
    Text("\(age)")
        .fontWeight(.bold)
    // ...
    
    // ...
    Text(status.rawValue)
        .fontWeight(.bold)
        .foregroundColor(status.color)
    // ...
}

Simplifying @AppStorage property declarations

Previous examples demonstrated passing explicitly the .standard value to the store parameter. However, @AppStorage property declarations can be simplified just by omitting which file to use, and by keeping the keys only:

@AppStorage("profileName") var profileName = "Not Set"
@AppStorage("age") var age = 40
@AppStorage("status") var status = OnlineStatus.Visible

In case we use the standard user defaults file for storing and reading data, the above will work just fine. If, however, there is a different user defaults file to use, then we can still keep the above, and add the following view modifier to the view that contains those properties marked with the @AppStorage wrapper:

VStack {
    // ...
}
.defaultAppStorage(UserDefaults(suiteName: "custom_user_defaults")!)

Avoiding errors with keys

As you've seen in the previous examples, it's necessary to provide the key of each piece of data that we want to read from user defaults. However, the more we use @AppStorage properties in an app, the higher the risk to make typo errors in keys. Even though it doesn't sound so important, it might lead to unnecessary loss of time trying to figure out why reading from user defaults or writing to it doesn't work properly.

So, in order to avoid all that and make sure that no accidental errors will ever occur while typing key names, I personally prefer to define an enum, cases of which represent the actual keys I want to use.

For the current example demonstrated in this post, the following enum is what I'd define:

enum StorageKeys: String {
    case profileName, age, status
}

Notice that the enum's cases have matching String raw values. Now, we can use these, instead of actual keys when declaring the @AppStorage properties:

@AppStorage(StorageKeys.profileName.rawValue) var profileName = "Not Set"
@AppStorage(StorageKeys.age.rawValue) var age = 40
@AppStorage(StorageKeys.status.rawValue) var status = OnlineStatus.Visible

Following the above approach guarantees that there will be no accidental typo errors that will drive us crazy and make us spend valuable time with such non-important situations.

Writing to user defaults

With the @AppStorage property wrapper, writing to user defaults file has no difference compared to reading values from it. So, in this example, the view that allows to edit the profile data will contain the same, or similar @AppStorage declarations (depending on the case) to those already previously presented:

@AppStorage(StorageKeys.profileName.rawValue) var profileName = ""
@AppStorage(StorageKeys.age.rawValue) var age = 40
@AppStorage(StorageKeys.status.rawValue) var status = OnlineStatus.Visible

What actually differs is how we use the above properties. When we pass their binding values as arguments to various SwiftUI views and they get updated, changes are written directly to the user defaults file.

See, for instance, the next code snippet that allows users to edit the profile data; we provide the binding value of the @AppStorage properties everywhere, and on change, @AppStorage property wrapper will store them persistently:

NavigationView {
    VStack {
        Form {
            TextField("Profile name", text: $profileName)
            
            Picker("Age", selection: $age) {
                ForEach(Array(20...60), id: \.self) {
                    Text("\($0)")
                }
            }
                                
            Picker("Status", selection: $status) {
                ForEach(OnlineStatus.allCases, id: \.self) {
                    Text("\($0.rawValue)")
                }
            }
        }
                
        // ...
    }
}

Conclusion

So, that's all you need to know in order to start using the @AppStorage property wrapper in your SwiftUI projects and store to user defaults. As I've mentioned previously, it's mostly suitable for saving small bits of data, such as user settings. Note, however, that many developers prefer implementing their own storage solutions and avoid using user defaults at all. That's totally up to you, and a decision to make if you realize that @AppStorage does not meet your needs. Nevertheless, @AppStorage could be useful in many occasions, so give it a try now that you've learnt about it. Thank you for reading!