@SceneStorage vs @AppStorage (UserDefaults) - SwiftUI

Jan 3, 2021 4 min read
@SceneStorage vs @AppStorage (UserDefaults) - SwiftUI

SceneStorage

In simple words, it saves the state of your app so when the app is killed or switched it comes back to where you were before, same goes for the data, it stays the same.

If you want to save unique data for each of your screens, you should use SwiftUI’s @SceneStorage property wrapper. This works a bit like @AppStorage in that you provide it with a name to save things plus a default value, but rather than working with UserDefaults it instead gets used for state restoration – and it even works great with the kinds of complex multi-scene set ups we see so often in iPadOS.

Watch a demo:

AppStorage

SwiftUI has a dedicated property wrapper for reading values from UserDefaults, which will automatically reinvoke your view’s body property when the value changes. That is, this wrapper effectively watches a key in UserDefaults, and will refresh your UI if that key changes.

UserDefaults allows us to store small amount of user data directly attached to our app. There is no specific number attached to “small”, but you should keep in mind that everything you store in UserDefaults will automatically be loaded when your app launches – if you store a lot in there your app launch will slow down. To give you at least an idea, you should aim to store no more than 512KB in there.

UserDefaults is perfect for storing user settings and other important data – you might track when the user last launched the app, which news story they last read, or other passively collected information.

The default of an int will be 0 and the boolean false.

It takes iOS a little time to write your data to permanent storage. They don’t write updates immediately because you might make several back to back, so instead they wait some time then write out all the changes at once. How much time is another number we don’t know, but a couple of seconds ought to do it.

As a result of this, if you tap the button then quickly relaunch the app from Xcode, you’ll find your most recent tap count wasn’t saved.

The key String is case-sensitive just like regular Swift strings, and it’s important – we need to use the same key to read the data back out of UserDefaults

For example, this will watch UserDefaults for a “username” key, which will be set when the button is pressed:

struct ContentView: View {
    @AppStorage("username") var username: String = "Anonymous"

    var body: some View {
        VStack {
            Text("Welcome, \(username)!")

            Button("Log in") {
                username = "@twostraws"
            }
        }
    }
}

Changing username above will cause the new string to be written to UserDefaults immediately, while also updating the view.

@AppStorage will watch UserDefaults.standard by default, but you can also make it watch a particular app group if you prefer, like this:

@AppStorage("username", store: UserDefaults(suiteName: "group.com.hackingwithswift.unwrap")) var username: String = "Anonymous"
Important:@AppStorage writes your data to UserDefaults, which is not secure storage. As a result, you should not save any personal data using @AppStorage, because it’s relatively easy to extract.

Watch a demo:


Old ways

You can still use UserDefaults as before, it's just less convenient.

Get values:

let username = UserDefaults.standard.object(forKey: "username") as? String ?? ""

Set values:

UserDefaults.standard.set(username, forKey: "username")

Why are these useful?
Because in ViewModels you won't be able to use @AppStorage with Foundation, if you want to use it you'll have to import SwiftUI, so the above way is very useful to get those values without importing an entire library just for that.

Example 1 (Store tap count)

Screenshot
Reveal Code

import SwiftUI

struct ContentView: View {
    @State private var tapCount = UserDefaults.standard.integer(forKey: "Tap")

    var body: some View {
        Button("Tap count: \(tapCount)") {
            self.tapCount += 1
            UserDefaults.standard.set(self.tapCount, forKey: "Tap")
        }
    }
}

Example 2  - (Store Integer - using a ViewModel class)

Screenshot
ContentView

import SwiftUI

struct ContentView: View {
    
    @ObservedObject var eatTracker = TimeToEatTrackerViewModel()
    
    var body: some View {
        VStack(spacing: 30) {
            Button(action: {
                self.eatTracker.currentMeal = self.eatTracker.currentMeal + 1
                print(String(self.eatTracker.currentMeal))
            }){
                Text(String(self.eatTracker.currentMeal))
            }
            
        }
    }
}

Swift file #2
import Foundation

class TimeToEatTrackerViewModel: ObservableObject {
    
    @Published var currentMeal: Int = UserDefaults.standard.integer(forKey: "CurrentMeal") {
        didSet {
            UserDefaults.standard.set(self.currentMeal, forKey: "CurrentMeal")
        }
    }
    
}

Example 3  - (Store dictionary - using a ViewModel class)

This implementation is slightly different from the above since changing property wrapper content value does not trigger didSet, at least for now, Xcode 11.4, solution:

ContentView

import SwiftUI

struct ContentView: View {
    
    @ObservedObject var eatTracker = TimeToEatTrackerViewModel()
    
    var body: some View {
        
        VStack {
            Button(action: {
                var tmp = self.eatTracker.mealAndStatus
                tmp["Breakfast"] = "done"
                self.eatTracker.mealAndStatus = tmp
            }){
                Text(String(self.eatTracker.mealAndStatus["Breakfast"]!))
            }
        }
    }
}

Swift file #2
import Foundation

class TimeToEatTrackerViewModel: ObservableObject {
    @Published var mealAndStatus: [String: String] =
        UserDefaults.standard.dictionary(forKey: "mealAndStatus") as? [String: String] ?? ["Breakfast": "initial", "Snack": "notSet", "Lunch": "notSet", "Snack2": "notSet", "Dinner": "notSet"] {
        didSet {
            UserDefaults.standard.set(self.mealAndStatus, forKey: "mealAndStatus")
        }
    }
}

Great! Next, complete checkout for full access to ArturoFM.
Welcome back! You've successfully signed in.
You've successfully subscribed to ArturoFM.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info has been updated.
Your billing was not updated.