Swift and SwiftUI tutorials for Swift Developers

What is and how to use @AppStorage in SwiftUI

In modern application development, User Experience (UX) is everything. A fundamental aspect of good UX is the application’s ability to “remember” user preferences. Whether it’s dark mode, font size, or whether the user has already seen the onboarding screen, expecting the user to configure the app every time they open it is a fatal mistake.

Before SwiftUI, persisting these small pieces of information required a constant dance with UserDefaults, manually synchronizing the user interface with saved data. With the arrival of SwiftUI, Apple introduced @AppStorage, a Property Wrapper that simplifies this process in an elegant and reactive way.

In this comprehensive tutorial, we will explore what @AppStorage is, how it works under the hood, and how to implement it in real-world applications for iOS, macOS, and watchOS.


1. What exactly is @AppStorage?

To understand @AppStorage, we must first look at its ancestor: UserDefaults.

UserDefaults is a simple key-value database that stores lightweight configuration data (Strings, numbers, Booleans) in the device’s file system (.plist). It is fast and efficient for small data, but it isn’t “reactive.” In the old days, if you changed a value in UserDefaults, you had to manually tell the view to update.

@AppStorage is the magic bridge between UserDefaults and SwiftUI’s declarative interface.

Key Features:

  • Reactivity: It works like @State. When the value on disk changes, @AppStorage automatically invalidates the view, and SwiftUI re-renders it with the new value.
  • Simplicity: It eliminates the need to write UserDefaults.standard.set(...) or UserDefaults.standard.integer(forKey: ...).
  • Source of Truth: It becomes a “Source of Truth” for user settings that persist between app launches.

Important Note: @AppStorage is not designed for complex databases (like a list of 5,000 tasks). For that, you have CoreData, SwiftData, or Realm. @AppStorage is strictly for preferences and settings.


2. Syntax and Supported Types

The anatomy of an @AppStorage declaration is simple, yet powerful.

@AppStorage("unique_key_on_disk") var variableName: Type = defaultValue

Breakdown of components:

  1. The Key: The string that identifies the value in the UserDefaults file. If two views use the same key, they will share the same data.
  2. The Variable: The name you will use inside your Swift code.
  3. The Type: Must be explicit or inferred.
  4. Default Value: The value the variable will hold if it is the first time the app is run and nothing is saved on disk.

Supported Data Types

Natively, @AppStorage supports the same types as UserDefaults:

  • Bool
  • Int
  • Double
  • String
  • URL
  • Data

3. Practical Implementation: Building a Settings Screen

Let’s build a practical example. Imagine a reading application where the user can configure:

  1. Whether to receive notifications (Bool).
  2. Their username (String).
  3. The reading font size (Double).

Step 1: Configuring the variables

Inside your SwiftUI View (e.g., SettingsView.swift), we declare the properties:

import SwiftUI

struct SettingsView: View {
    // 1. Persisting a Boolean
    @AppStorage("notificationsEnabled") private var notificationsEnabled = true
    
    // 2. Persisting a String
    @AppStorage("username") private var username = "Guest"
    
    // 3. Persisting a Double
    @AppStorage("fontSize") private var fontSize = 14.0

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Profile")) {
                    // The binding ($) automatically writes to UserDefaults
                    TextField("Username", text: $username)
                }
                
                Section(header: Text("Preferences")) {
                    Toggle("Receive Notifications", isOn: $notificationsEnabled)
                    
                    VStack(alignment: .leading) {
                        Text("Font size: \(Int(fontSize)) pts")
                        Slider(value: $fontSize, in: 10...30, step: 1)
                    }
                }
                
                Section {
                    // Button to reset (simple business logic)
                    Button("Reset values") {
                        notificationsEnabled = true
                        username = "Guest"
                        fontSize = 14.0
                    }
                    .foregroundColor(.red)
                }
            }
            .navigationTitle("Settings")
        }
    }
}

What is happening here?

When the user moves the Slider or types in the TextField:

  1. The value changes in memory.
  2. SwiftUI immediately writes the new value to UserDefaults under the assigned key.
  3. If you close the app (kill it from multitasking) and reopen it, the controls will appear exactly as you left them. Zero extra load or save code.

4. Advanced Persistence: Enums and Structs

This is where many developers get stuck. What if I want to save an option from an enum or a complex struct?

Saving Enumerations (Enums)

To save an enum in @AppStorage, the enum must conform to the String (or Int) protocol. CaseIterable is optional but useful for Pickers.

enum AppTheme: String, CaseIterable, Identifiable {
    case system = "System"
    case light = "Light"
    case dark = "Dark"
    
    var id: String { self.rawValue }
}

struct AppearanceView: View {
    // Swift understands it needs to save the 'rawValue' (String) of the enum
    @AppStorage("appTheme") private var selectedTheme: AppTheme = .system
    
    var body: some View {
        Picker("App Theme", selection: $selectedTheme) {
            ForEach(AppTheme.allCases) { theme in
                Text(theme.rawValue).tag(theme)
            }
        }
    }
}

Saving Structs and Complex Objects (JSON)

@AppStorage does not support Structs or Classes directly. If you try @AppStorage("user") var user: UserStruct, the compiler will throw an error.

To solve this, we need to conform to RawRepresentable and use JSONEncoder to convert the object into a String or Data that UserDefaults understands.

Here is a reusable extension you can copy and paste into your projects:

import SwiftUI

// Extension to allow saving any Codable object in AppStorage
extension Array: RawRepresentable where Element: Codable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode([Element].self, from: data)
        else {
            return nil
        }
        self = result
    }

    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return "[]"
        }
        return result
    }
}

// Usage Example
struct Task: Codable, Identifiable, Hashable {
    var id = UUID()
    var title: String
    var isCompleted: Bool
}

struct TaskListView: View {
    // Now we can save an Array of tasks directly
    @AppStorage("savedTasks") var tasks: [Task] = []
    
    var body: some View {
        List {
            ForEach(tasks) { task in
                Text(task.title)
            }
        }
        .onAppear {
            // Add a test task if empty
            if tasks.isEmpty {
                tasks.append(Task(title: "Learn SwiftUI", isCompleted: false))
            }
        }
    }
}

Performance Warning: Encoding and decoding JSON on every keystroke or small change can be expensive if the object is very large. Use this sparingly.


5. @AppStorage in the Apple Ecosystem (Multiplatform)

One of the beauties of SwiftUI is its portability. @AppStorage works identically on iOS, macOS, watchOS, and tvOS, but there are specific UX considerations.

On macOS: The Preferences Window

On macOS, the standard is to have a dedicated Settings window (formerly Preferences).

@main
struct MyAppMac: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        
        // Defines the standard macOS settings scene
        Settings {
            SettingsView() // The same view we used in iOS
                .frame(width: 400, height: 300)
        }
    }
}

By using @AppStorage in SettingsView, any changes you make in that window will instantly propagate to ContentView or other open windows, as they all observe the same key in UserDefaults.

On watchOS

On the Apple Watch, interactions are short. @AppStorage is perfect for saving the state of a workout session or a display preference. However, avoid saving large amounts of data here to prevent impacting battery life.


6. Sharing Data Between App and Widgets (App Groups)

If you are creating a Home Screen Widget, you will realize that your Widget cannot read your main app’s @AppStorage by default. This is because each executable lives in its own Sandbox.

To share data, you need to use App Groups.

  1. Xcode: Go to “Signing & Capabilities” in your main target and in the Widget target.
  2. Add Capability: Add “App Groups”.
  3. Create Group: Create an identifier, e.g., group.com.mycompany.myapp.

Now, you must tell @AppStorage to use that group instead of the standard one.

// Define the shared store
extension UserDefaults {
    static let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")
}

struct WidgetView: View {
    // Inject the store explicitly
    @AppStorage("dailyGoal", store: UserDefaults.shared) var dailyGoal = 500
    
    var body: some View {
        Text("Goal: \(dailyGoal)")
    }
}

By specifying the store parameter, both the main app and the widget read and write to the same physical file.


7. Common Mistakes and Best Practices

Despite its ease of use, there are traps that are easy to fall into.

A. Don’t use Magic Keys (Magic Strings)

Repeating "username" in five different views is a recipe for disaster (typos).

Solution: Create a String extension or a struct of constants.

enum AppStorageKeys {
    static let username = "username_key"
    static let isDarkMode = "dark_mode_key"
}

// Usage
@AppStorage(AppStorageKeys.username) var username = "..."

B. Sensitive Data

NEVER store passwords, API tokens, or health information in @AppStorageUserDefaults is not securely encrypted and is easy to extract if someone has access to the unlocked device.

  • Use: Keychain for passwords.
  • Use: @AppStorage only for UI preferences (color, size, options).

C. The Update Cycle Problem

If you have two different views editing the same @AppStorage at the same time, SwiftUI handles synchronization well, but be careful with logic loops (View A changes data -> View B reacts and changes data -> View A reacts…).

D. Unit Testing

Testing views with @AppStorage can be tricky because the state persists between tests. Tip: For your tests, inject an in-memory UserDefaults or clean UserDefaults.standard in the setUp or tearDown method of your tests.

// In your tests
override func tearDown() {
    let domain = Bundle.main.bundleIdentifier!
    UserDefaults.standard.removePersistentDomain(forName: domain)
}

8. AppStorage vs SwiftData vs CoreData

To conclude, it is vital to know when to stop using @AppStorage.

Feature@AppStorage (UserDefaults)SwiftData / CoreData
Data VolumeSmall (< 1MB recommended)Large (Gigabytes)
StructureFlat (Key-Value)Relational (Graphs, Tables)
ComplexityVery LowMedium / High
SpeedInstant (synchronous)Asynchronous / Optimized
QueriesNo (read by key only)Yes (filters, sorting)
Ideal UseSettings, Flags (“First time seen”)User lists, Inventories, Offline Cache

Conclusion

@AppStorage is one of those tools in SwiftUI that makes you wonder how we ever lived without it. It massively reduces boilerplate code and integrates perfectly with the framework’s reactive philosophy.

By mastering @AppStorage, you aren’t just saving data; you are creating a fluid and personalized user experience that makes your application feel professional and attentive to user needs on iOS, macOS, and watchOS.

Always remember: use it for preferences, use Keychain for secrets, and SwiftData for your main database. With that balance, your data architecture will be solid as a rock.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Best Xcode Extensions

Next Article

How to write Unit Tests in Swift

Related Posts