@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 toUserDefaults
, 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
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
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:
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") } } }