- Published on
- 8 min read
> @AppStorage vs UserDefaults vs SwiftData: Choosing the Right Persistence
Every app needs to save data somewhere. Maybe it's a user's preferred theme, a list of favorite items, or an entire database of records. Swift gives you several options for local persistence, and picking the right one depends on what you're storing and how you plan to use it.
The three most common choices are @AppStorage, UserDefaults, and SwiftData. They each serve different purposes, but there's enough overlap that it's easy to reach for the wrong one. Let's walk through each approach and see where they shine.
@AppStorage: SwiftUI's Built-in Preference Binding
@AppStorage is a SwiftUI property wrapper that reads and writes values to UserDefaults automatically. It's the simplest persistence option and it integrates directly with SwiftUI's view update system, so your UI refreshes whenever the stored value changes.
import SwiftUI
struct SettingsView: View {
@AppStorage("isDarkMode") private var isDarkMode = false
@AppStorage("username") private var username = "Guest"
@AppStorage("fontSize") private var fontSize = 16.0
var body: some View {
Form {
Toggle("Dark Mode", isOn: $isDarkMode)
TextField("Username", text: $username)
Slider(value: $fontSize, in: 12...24, step: 1) {
Text("Font Size: \(Int(fontSize))")
}
}
}
}
That's all it takes. No save buttons, no manual encoding. The value persists across app launches and updates the view automatically.
@AppStorage supports a limited set of types out of the box: Bool, Int, Double, String, URL, and Data. If you want to store something like an enum, you can conform it to RawRepresentable:
enum AppTheme: String {
case system
case light
case dark
}
struct ThemePickerView: View {
@AppStorage("selectedTheme") private var selectedTheme: AppTheme = .system
var body: some View {
Picker("Theme", selection: $selectedTheme) {
Text("System").tag(AppTheme.system)
Text("Light").tag(AppTheme.light)
Text("Dark").tag(AppTheme.dark)
}
}
}
The limitation here is real though. @AppStorage is meant for simple preferences. If you try to store anything complex by encoding it to Data, you lose the simplicity that made it appealing in the first place.
UserDefaults: The Traditional Approach
UserDefaults has been around since the early days of iOS development. It's a key-value store backed by a property list file, and it works well for small pieces of data like settings, flags, and user preferences.
// Writing values
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
UserDefaults.standard.set(42, forKey: "highScore")
UserDefaults.standard.set(["Swift", "Kotlin"], forKey: "favoriteLanguages")
// Reading values
let completed = UserDefaults.standard.bool(forKey: "hasCompletedOnboarding")
let score = UserDefaults.standard.integer(forKey: "highScore")
let languages = UserDefaults.standard.stringArray(forKey: "favoriteLanguages") ?? []
Where UserDefaults becomes more useful than @AppStorage is when you need to persist data outside of SwiftUI views. You might be saving state in a network manager, caching a token in a service class, or tracking analytics preferences in your app delegate. @AppStorage is a SwiftUI property wrapper, so it only works inside views.
You can also store Codable objects by encoding them yourself:
struct UserPreferences: Codable {
var notificationsEnabled: Bool
var dailyReminderTime: Date
var preferredCategories: [String]
}
class PreferencesManager {
private let key = "userPreferences"
func save(_ preferences: UserPreferences) {
if let data = try? JSONEncoder().encode(preferences) {
UserDefaults.standard.set(data, forKey: key)
}
}
func load() -> UserPreferences {
guard let data = UserDefaults.standard.data(forKey: key),
let preferences = try? JSONDecoder().decode(UserPreferences.self, from: data) else {
return UserPreferences(
notificationsEnabled: true,
dailyReminderTime: Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: Date())!,
preferredCategories: []
)
}
return preferences
}
}
One thing to keep in mind: UserDefaults is not a database. Apple's documentation specifically warns against storing large amounts of data in it. The entire contents get loaded into memory, so if you start cramming large arrays or image data in there, you'll see performance issues. A good rule of thumb is to keep the total size under a few hundred kilobytes.
SwiftData: Structured Persistence for Real Data
When your data has relationships, needs querying, or involves more than a handful of records, SwiftData is the right tool. Introduced at WWDC 2023, it replaces Core Data with a much cleaner API built around Swift macros and modern concurrency.
You define your data model with the @Model macro:
import SwiftData
@Model
class Recipe {
var title: String
var ingredients: [String]
var instructions: String
var isFavorite: Bool
var dateCreated: Date
@Relationship(deleteRule: .cascade)
var tags: [Tag]
init(title: String, ingredients: [String], instructions: String) {
self.title = title
self.ingredients = ingredients
self.instructions = instructions
self.isFavorite = false
self.dateCreated = Date()
self.tags = []
}
}
@Model
class Tag {
var name: String
init(name: String) {
self.name = name
}
}
SwiftData handles the storage, schema migrations, and relationship management. You query your data using the @Query macro in SwiftUI:
struct RecipeListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Recipe.dateCreated, order: .reverse) private var recipes: [Recipe]
var body: some View {
NavigationStack {
List(recipes) { recipe in
RecipeRow(recipe: recipe)
.swipeActions {
Button(role: .destructive) {
context.delete(recipe)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
.navigationTitle("Recipes")
.toolbar {
Button("Add") {
let recipe = Recipe(
title: "New Recipe",
ingredients: [],
instructions: ""
)
context.insert(recipe)
}
}
}
}
}
You set up the model container at the app level:
@main
struct RecipeApp: App {
var body: some Scene {
WindowGroup {
RecipeListView()
}
.modelContainer(for: [Recipe.self, Tag.self])
}
}
@Query supports filtering with predicates, which makes it easy to build search and filter features:
struct FavoriteRecipesView: View {
@Query(
filter: #Predicate<Recipe> { $0.isFavorite },
sort: \Recipe.title
)
private var favorites: [Recipe]
var body: some View {
List(favorites) { recipe in
Text(recipe.title)
}
.navigationTitle("Favorites")
}
}
SwiftData requires iOS 17+, so if you're still supporting older versions, you'll need to stick with Core Data or one of the other options for now.
When to Use Each
The decision usually comes down to what kind of data you're storing and where you need to access it.
@AppStorage is perfect for individual settings that drive your UI. Think toggles, theme preferences, or small configuration values that a SwiftUI view needs to both read and write. It's the least amount of code for the most common case.
UserDefaults covers the same territory but works everywhere, not just in SwiftUI views. Use it when you need to read or write preferences from a background service, an app extension, or anywhere outside the view layer. It also lets you share data between your main app and widgets or extensions through app groups.
SwiftData is for structured, queryable data. If you're building a notes app, a recipe manager, a workout tracker, or anything where users create and manage collections of records, that's SwiftData territory. It handles relationships between objects, supports sorting and filtering, and persists everything to an SQLite database under the hood.
Here's a practical example showing all three in one app:
import SwiftUI
import SwiftData
// SwiftData model for structured data
@Model
class JournalEntry {
var text: String
var date: Date
var mood: String
init(text: String, mood: String) {
self.text = text
self.date = Date()
self.mood = mood
}
}
struct JournalView: View {
// @AppStorage for a simple UI preference
@AppStorage("showDates") private var showDates = true
// SwiftData query for structured records
@Query(sort: \JournalEntry.date, order: .reverse) private var entries: [JournalEntry]
@Environment(\.modelContext) private var context
var body: some View {
NavigationStack {
List(entries) { entry in
VStack(alignment: .leading, spacing: 4) {
Text(entry.text)
if showDates {
Text(entry.date, style: .date)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle("Journal")
.toolbar {
Toggle("Show Dates", isOn: $showDates)
}
}
}
}
In this journal app, @AppStorage handles the display preference (whether to show dates), while SwiftData manages the actual journal entries. If this app also had a share extension that needed to check a preference, you'd use UserDefaults with an app group for that piece.
A Quick Note on Alternatives
These three aren't the only options. Keychain is the right choice for sensitive data like passwords and tokens. File-based storage (writing JSON or plist files to the documents directory) works well for large blobs of data or when you need precise control over the file format. And if you need a full relational database with complex queries, you can always use SQLite directly through packages like GRDB.
But for the vast majority of apps, some combination of @AppStorage, UserDefaults, and SwiftData covers everything you need. Start with the simplest option that fits your data, and upgrade to a more capable solution when you outgrow it.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
Understanding the "some" Keyword in Swift and SwiftUI
Learn what the "some" keyword means in Swift, how opaque return types work, and why SwiftUI uses "some View" everywhere.
Using Failable Initializers to Handle Optionals in SwiftUI Views
Failable initializers offer a cleaner alternative to if-let statements when your SwiftUI views depend on optional data.
Detecting When App Enters Background and Saving State in SwiftUI
Learn how to detect app lifecycle changes in SwiftUI, save state when your app enters the background, and restore it when users return, using scenePhase and UIApplication notifications.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.