Data persistence using AppStorage and SceneStorage in SwiftUI iOS 24.05.2021

It is a common requirement for an app to need to store small amounts of data which will persist through app restarts. This is particularly useful for storing user preference settings, or when restoring a scene to the exact state it was in last time it was accessed by the user. SwiftUI provides two property wrappers (@AppStorage and @SceneStorage) for the specific purpose of persistently storing small amounts of app data.

SceneStorage

The @SceneStorage property wrapper is used to store small amounts of data within the scope of individual app scene instances and is ideal for saving and restoring the state of a screen between app launches. Consider a situation where a user, partway through entering information into a form within an app, is interrupted by a phone call or text message, and places the app into the background.

Scene storage is declared using the @SceneStorage property wrapper together with a key string value which is used internally to store the associated value. The following code, for example, declares a scene storage property designed to store a String value using a key name set to occupation with an initial default value set to an empty string:

@SceneStorage("occupation") var occupation: String = ""

Once declared, the stored property could, for example, be used in conjunction with a TextEditor as follows:

var body: some View {
    TextEditor(text: $occupation).padding()
}

When implemented in an app, this will ensure that any text entered into the text field is retained within the scene through app restarts.

When working with scene storage it is important to keep in mind that each instance of a scene has its own storage which is entirely separate from any other scenes.

AppStorage

The @SceneStorage property wrapper allows each individual scene within an app to have its own copy of stored data. In other words, the data stored by one scene is not accessible to any other scenes in the app (even other instances of the same scene).

The @AppStorage property wrapper, on the other hand, is used to store data that is universally available throughout the entire app.

App Storage is built on top of UserDefaults, a feature which has been available in iOS for many years. Primarily provided as a way for apps to access and store default user preferences (such as language preferences or color choices).

@AppStorage property wrapper requires a string value to serve as a key and may be declared as follows:

@AppStorage("language") var language: String = ""

By default, data will be stored in the standard UserDefaults storage. It is also possible, however, to specify a custom App Group in which to store the data.

Storing Custom Types

The @AppStorage and @SceneStorage property wrappers only allow values of certain types to be stored. Specifically Bool, Int, Double, String, URL and Data types. This means that any other type that needs to be stored must first be encoded as a Swift Data object in order to be stored and subsequently decoded when retrieved.

Consider, for example, the following struct declaration and initialization:

struct Movie {
    var title: String
    var year: Int
}

var cinema = Movie(title: "The Godfather", secondName: "1972")

The instance needs to be encoded and encapsulated into a Data instance before it can be saved. The exact steps to perform the encoding and decoding will depend on the type of the data being stored. The key requirement, however, is that the type conforms to the Encodable and Decodable protocols.”

struct Movie: Encodable, Decodable {
    var title: String
    var year: Int
}

The following example uses a JSON encoder to encode cinema instance and store it using the @AppStorage property wrapper:

@AppStorage("cinema") var cinemaStore: Data = Data()

let encoder = JSONEncoder()

if let data = try? encoder.encode(cinema) {
    cinemaStore = data
}

When the time comes to retrieve the data from storage, the process is reversed using the JSON decoder:

let decoder = JSONDecoder()

if let movie = try? decoder.decode(Movie.self, from: cinemaStore) {
    cinema = movie
}