@AppStorage in SwiftUI is a property wrapper for working with UserDefaults that automatically syncs the value with the UI. When a property declared with @AppStorage changes, the new value is immediately saved to UserDefaults, and when UserDefaults changes externally — by a widget or extension — the View automatically redraws. According to Apple Developer Documentation (2025), @AppStorage supports String, Int, Double, Bool, Data, URL and their optional versions, providing reactive storage of user settings without manual observation code.
Key Takeaways
@AppStorage is a property wrapper introduced by Apple in iOS 14 that binds a View property to a key in UserDefaults. When reading the property, SwiftUI loads the value from UserDefaults by the specified key. When writing, it saves the new value and notifies the View that it needs to redraw.
Before @AppStorage, developers had to manually read UserDefaults in onAppear, subscribe to UserDefaults.didChangeNotification, and update @State on changes. @AppStorage automates the entire cycle: a one-line declaration replaces 15–20 lines of boilerplate code. Moreover, @AppStorage provides bidirectional synchronization — if the UserDefaults value changes from another process (e.g., App Extension or Widget), the View will still receive the update.
Architecturally, @AppStorage is implemented as DynamicProperty, which allows SwiftUI to track dependencies and redraw the View when the observed value changes. This makes it ideal for storing user settings: interface language, enabling/disabling features, last selected tab, user name.
Although @AppStorage uses UserDefaults under the hood, the approaches to working with storage are fundamentally different. UserDefaults is a low-level API that requires manual management of reading, writing, and change notifications. @AppStorage is a SwiftUI abstraction that provides reactive behavior out of the box.
UserDefaults is suitable for one-time operations: loading settings at app startup, writing analytics, caching tokens. @AppStorage is for settings that should reactively update the UI: theme toggles, language selection, saving interface state. Using UserDefaults directly inside a View is an anti-pattern, as the View does not know about changes without additional subscription.
| Parameter | @AppStorage | UserDefaults |
|---|---|---|
| Reactivity | Automatic | Requires subscription to notifications |
| Boilerplate | 1 line per property | 15–20 lines per property |
| Types | String, Int, Double, Bool, Data, URL | All types + archived objects |
| Custom types | Via RawRepresentable | Via NSKeyedArchiver |
| App Extension | Automatic sync | Manual subscription |
For simple settings with reactive UI @AppStorage is the preferred choice. For complex data (arrays, dictionaries, custom objects) use a combination of UserDefaults with @State and manual change subscription, or switch to SwiftData / Core Data for structured storage.
@AppStorage supports standard types that UserDefaults can serialize directly: String, Int, Double, Bool, Data, URL. For each type there is an optional version (String?, Int?, Double?, Bool?, Data?, URL?), allowing you to distinguish between "not set" and "empty value".
For storing custom types that conform to the RawRepresentable protocol, @AppStorage also works automatically. If an enum has a rawValue of type String or Int, it can be used directly: @AppStorage("theme") var theme: AppTheme = .system. SwiftUI automatically serializes/deserializes the value via rawValue.
enum AppTheme: String {
case system, light, dark
}
struct SettingsView: View {
@AppStorage("username") var username: String = "Guest"
@AppStorage("launchCount") var launchCount: Int = 0
@AppStorage("isDarkMode") var isDarkMode: Bool = false
@AppStorage("appTheme") var theme: AppTheme = .system
@AppStorage("lastOpened") var lastOpened: Date? = nil
var body: some View {
Form {
TextField("Username", text: $username)
Toggle("Dark mode", isOn: $isDarkMode)
Text("Launched \(launchCount) times")
}
}
}
The example uses different @AppStorage types: String with a default value of "Guest", Int for a launch counter, Bool for dark theme, AppTheme enum with rawValue of type String, and an optional Date? for the last open time. Each property is bound to a UserDefaults key specified as the first argument. The default value is used if the key is missing from storage on first launch.
One of the key advantages of @AppStorage is automatic observation of UserDefaults changes from any source. If an App Extension or Widget changes a value, @AppStorage in the parent app receives the notification and redraws the View. This is achieved through the KVO (Key-Value Observing) mechanism that @AppStorage automatically sets up on UserDefaults.didChangeNotification.
In practice, this means that if the user changes a setting in a Widget (e.g., enables dark theme), the app immediately picks up the change. The same synchronization works between the main app and Share Extension, Watch App or Today Widget. The developer does not need to write code for interprocess data exchange — @AppStorage does it automatically.
struct ThemeSettingView: View {
@AppStorage("isDarkMode") var isDarkMode: Bool = false
var body: some View {
VStack {
Toggle("Dark Mode", isOn: $isDarkMode)
.onChange(of: isDarkMode) { oldValue, newValue in
print("Dark mode changed to \(newValue)")
}
}
}
}
Toggle is bound to $isDarkMode via @AppStorage. When toggled, the value is automatically saved to UserDefaults under the key "isDarkMode". The .onChange modifier allows performing a side effect on change — for example, sending analytics or updating the UI of other screens. If a Widget changes the same key, @AppStorage will also trigger onChange, ensuring state consistency.
Let's look at a full-fledged app settings screen that uses @AppStorage to store all configurations. The form contains sections with different types of settings: text fields, toggles, counters — all values are automatically saved to UserDefaults.
struct AppSettingsView: View {
@AppStorage("displayName") var displayName = ""
@AppStorage("notificationsEnabled") var notificationsEnabled = true
@AppStorage("maxResults") var maxResults = 25
@AppStorage("selectedTab") var selectedTab = "home"
var body: some View {
NavigationStack {
Form {
Section(header: Text("Profile")) {
TextField("Display name", text: $displayName)
}
Section(header: Text("Preferences")) {
Toggle("Enable notifications",
isOn: $notificationsEnabled)
Stepper("Max results: \(maxResults)",
value: $maxResults,
in: 10...100,
step: 5)
}
Section {
Button("Reset settings") {
UserDefaults.standard.removePersistentDomain(
forName: Bundle.main.bundleIdentifier!)
}
.tint(.red)
}
}
.navigationTitle("Settings")
}
}
}
The form contains four @AppStorage properties of different types: String for name, Bool for notifications, Int for result count and String for selected tab. All controls are bound to properties via Binding ($displayName, $notificationsEnabled, etc.). The "Reset settings" button clears all UserDefaults by removing the app domain — after that @AppStorage automatically returns to the default values.
struct SharedSettingsView: View {
let sharedDefaults = UserDefaults(suiteName: "group.com.example.app")
@AppStorage("widgetTheme", store: UserDefaults(suiteName: "group.com.example.app")!)
var widgetTheme: String = "system"
@AppStorage("widgetColor", store: UserDefaults(suiteName: "group.com.example.app")!)
var widgetColor: String = "blue"
var body: some View {
Form {
Picker("Widget theme", selection: $widgetTheme) {
Text("System").tag("system")
Text("Light").tag("light")
Text("Dark").tag("dark")
}
Picker("Accent color", selection: $widgetColor) {
Text("Blue").tag("blue")
Text("Green").tag("green")
Text("Red").tag("red")
}
}
}
}
For App Group (shared storage between the app and extensions) @AppStorage accepts the store parameter: UserDefaults(suiteName:). Values are saved in the shared container available to the main app, Widget, Watch App and other extensions of the same group. A Widget can read these settings, and when they change in the app, the Widget automatically updates through the UserDefaults observation mechanism.
Frequently Asked Questions
@State stores the value only in memory and resets when the app restarts. @AppStorage saves the value in UserDefaults and restores it on the next launch. Use @State for temporary screen data, @AppStorage for settings that should survive a restart.
Yes, if the Enum conforms to the RawRepresentable protocol with rawValue of type String or Int. Example: @AppStorage("theme") var theme: AppTheme = .system. SwiftUI automatically serializes the enum via rawValue and restores it on load.
Call UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) for the standard storage or removeObject(forKey:) for a specific key. After clearing, all @AppStorage properties will return to the default values specified in the declaration.
Yes, for synchronization between the app and extensions use App Group: @AppStorage("key", store: UserDefaults(suiteName: "group.com.example.app")!). Widget, Share Extension and Watch App can read and write to the same UserDefaults, and changes are automatically tracked.
@AppStorage uses UserDefaults, which is designed for small amounts of data: settings, tokens, counters. The recommended limit is up to 100 KB per app. For structured or large data (object arrays, media files) use SwiftData, Core Data or the file system.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also