All articles
iOS21 min read

SwiftUI Storing Data in 2025: SwiftData, Core Data, AppStorage & SceneStorage Explained

Need to store user settings, session data, or a full-blown object graph in your SwiftUI app? This 2025 guide breaks down all your options —…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

🔰 Introduction

In every real-world iOS app, you eventually hit that moment:

“Okay, but where do I _save_ this thing?”

Whether it’s a theme toggle, a user’s login state, a list of favorite items, or an entire database of models — data persistence is the secret sauce that makes apps feel intelligent, personalized, and, well… not like a goldfish 🐠 with memory issues.

In SwiftUI, Apple gives you multiple options to persist data — each tailored for different jobs:

💡 And this article is fully updated with what’s new in SwiftData from WWDC 2025 — including model inheritance, better predicate filtering, and smoother stability.

So whether you’re building a simple to-do app or scaling up to something more ambitious, let’s dive into all the tools you can use to store and retrieve data in SwiftUI like a pro.

💾 @AppStorage: The Global Saver for Simple Settings

Need to remember a simple preference like dark mode? A language toggle? Whether the user has already seen your onboarding flow? You don’t need a database, or even SwiftData — you just need @AppStorage.

This is SwiftUI’s elegant way of binding small, persistent values to your UI — with zero boilerplate.

🧠 What Is @AppStorage?

@AppStorage is a property wrapper that connects a Swift variable to UserDefaults under the hood.

It lets you save and retrieve basic types (Bool, String, Int, etc.) using a key, and SwiftUI:

✅ When Should You Use It?

Use @AppStorage when:

🧪 Example:

struct SettingsView: View {
    @AppStorage("isDarkMode") private var isDarkMode = false

    var body: some View {
        Toggle("Enable Dark Mode", isOn: $isDarkMode)
    }
}

⚙️ How It Actually Works

Here’s the magic behind the scenes:

  1. On first launch, if the key “isDarkMode” doesn’t exist in UserDefaults, the default (false) is used.
  2. If the user toggles it to true, SwiftUI:

3\. On future launches, SwiftUI finds “isDarkMode” already stored and uses that value, ignoring the default.

✅ So your value persists automatically — no save/load code required.

🔥 Pro Tips

🗂️ @SceneStorage: UI State That Lives With the View

Ever had a user switch away from your app mid-task — only to return and see their scroll position reset or selected tab lost? 😩 That’s where @SceneStorage comes to the rescue.

This property wrapper helps your SwiftUI views remember small pieces of transient UI state — like which tab was selected or what the user had typed into a field — as long as the scene is alive.

🧠 What Is @SceneStorage?

@SceneStorage lets SwiftUI automatically store and restore view-specific state values during a scene’s lifecycle (i.e., while the app is open or suspended).

Think of it like temporary memory for your views — it keeps things in place when users multitask, lock their phone, or temporarily switch apps.

It’s backed by the system’s scene restoration mechanism, not UserDefaults.

✅ When Should You Use It?

🧪 Example:

struct ContentView: View {
    @SceneStorage("selectedTab") private var selectedTab = "home"

    var body: some View {
        TabView(selection: $selectedTab) {
            HomeView()
                .tabItem { Label("Home", systemImage: "house") }
                .tag("home")

            SettingsView()
                .tabItem { Label("Settings", systemImage: "gear") }
                .tag("settings")
        }
    }
}

💡 When the app is suspended or sent to the background, the selected tab is preserved. If the system closes and restores the scene later, the user lands exactly where they left off. 🪄

🧪 Key Behavior to Know

🔥 Pro Tips

🧱 SwiftData: The Future of Persistence

If you’ve ever wrestled with Core Data’s boilerplate or questioned why saving a simple object required ritual sacrifices to Xcode’s model editor… welcome to the SwiftData era.

SwiftData is Apple’s modern persistence framework introduced in iOS 17, designed to work natively with Swift and SwiftUI. It’s declarative, uses macros, supports relationships, and plays nicely with concurrency — all while hiding the scary bits under the hood. And as of WWDC 2025, it’s more powerful than ever.

🧠 What Is SwiftData?

At its core, SwiftData is:

In simpler terms: it gives you real persistence with real Swift syntax and real joy instead of frustration.

🪄 What Makes It Special?

✍️ Working with SwiftData: Create, Read, Update, Delete

If you’re used to Core Data’s ceremonies — NSManagedObjectContext, NSFetchRequest, save() calls, and endless crashes because you forgot .context.insert() — SwiftData is here to simplify your life.

In SwiftData, you work with plain Swift model objects and modify them like normal classes. Behind the scenes, SwiftData takes care of tracking, persistence, and syncing with your UI.

Let’s walk through each CRUD operation in SwiftData using a simple Task model.

✅ Model Setup

@Model
class Task {
    var title: String
    var isDone: Bool

    init(title: String, isDone: Bool = false) {
        self.title = title
        self.isDone = isDone
    }
}

Your model is just a Swift class marked with @Model. No inheritance, no context passing — it's Swift-native.

📌 Injecting SwiftData

Make sure you’ve set up .modelContainer(for:) in your @main app to enable persistence and inject modelContext.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Task.self)
    }
}

1️⃣ Create a new Task

To create and save a new object, just create an instance and insert it into the context.

@Environment(\.modelContext) private var modelContext

Button("Add Task") {
    let task = Task(title: "Write SwiftData article")
    modelContext.insert(task)
}

2️⃣ Read (Fetch All)

Use @Query in your views to get a live, auto-updating list of data:

@Query var tasks: [Task]

var body: some View {
    List(tasks) { task in
        Text(task.title)
    }
}

This will update in real-time when you add/remove/update any Task.

Want to fetch all tasks manually (not bound to view)?

let tasks = try modelContext.fetch(FetchDescriptor<Task>())

3️⃣ Read (Fetch Conditionally)

If you want to fetch a filtered set of records, use FetchDescriptor with a #Predicate.

Example: Fetch tasks that are not done

let pendingTasks = try modelContext.fetch(
    FetchDescriptor<Task>(
        predicate: #Predicate { !$0.isDone }
    )
)

Example: Fetch tasks that contain a keyword

let descriptor = FetchDescriptor<Task>(
    predicate: #Predicate { $0.title.localizedStandardContains("urgent") },
    sortBy: [SortDescriptor(\.title)]
)

let urgentTasks = try modelContext.fetch(descriptor)

✅ You can also limit results:

descriptor.fetchLimit = 10

✅ And combine multiple filters:

#Predicate { $0.isDone == false && $0.title.contains("meeting") }

4️⃣ Update an existing Task

You don’t need any special calls to update a row. Just modify the object and SwiftData tracks the changes automatically.

Example:

@Environment(\.modelContext) private var modelContext

func markAsDone(_ task: Task) {
    task.isDone = true
    // No need to call save — SwiftData persists this automatically
}

💡 Important: You must be working with the actual tracked instance (i.e., the one you fetched or passed into the view from @Query). If you copy or recreate the object, SwiftData won’t track it.

Bonus: Updating inside a view

Use @Bindable to allow SwiftUI to mutate SwiftData models and persist automatically:

struct TaskRow: View {
    @Bindable var task: Task

    var body: some View {
        Toggle("Done", isOn: $task.isDone)
    }
}

SwiftData will update the database the moment task.isDone changes.

5️⃣ Delete a Task

Deleting is just as simple:

modelContext.delete(task)

This works whether you’re deleting from a view or some async method.

Example in a list:

List {
    ForEach(tasks) { task in
        Text(task.title)
    }
    .onDelete { indexSet in
        for index in indexSet {
            modelContext.delete(tasks[index])
        }
    }
}

✅ Summary: SwiftData CRUD at a Glance

⚠️ Gotchas and Things to Know

Now that you understand how SwiftData works, let’s zoom into the latest improvements announced at WWDC 2025 that make it even more powerful…

🚀 WWDC 2025 Highlights: What’s New in SwiftData

SwiftData launched with a strong foundation in iOS 17, but at WWDC 2025, Apple brought serious upgrades that make it ready for more advanced and real-world use cases.

If you tried SwiftData in its early days and felt a bit underwhelmed — now’s the time to take another look.

🆕 1. Model Inheritance Support

You can now create models that inherit from other models, allowing shared logic, properties, and structure — a major win for code reuse and clean architecture.

Example:

@Model class Note { // Super
    var title: String
}

@Model class TodoNote: Note { // Child
    var isCompleted: Bool
}

✅ Relationships and persistence work just like they do in base models — no extra configuration needed.

🆕 2. Codable Support in Predicates

Previously, you couldn’t use enums or Codable types in SwiftData predicates — now you can!

Example:

enum Status: String, Codable {
    case active, archived
}

@Model class Task {
    var status: Status
}

let activeTasks = try modelContext.fetch(
    FetchDescriptor<Task>(
        predicate: #Predicate { $0.status == .active }
    )
)

✅ Works with enums, optional enums, and other Codable properties.

🆕 3. Improved Persistent History Tracking

SwiftData now includes better support for persistent history, enabling:

While the API is mostly automatic under the hood, this makes SwiftData much more reliable in collaborative or complex editing flows.

🛠 4. Stability Fixes & Reliability Improvements

WWDC 2025 brought tons of stability fixes for issues devs had reported:

In short: if SwiftData previously felt kinda beta, it’s production-ready now — at least for most app types.

⚠️ Still Missing (As of WWDC 2025)

Not everything’s perfect yet. Some notable limitations still exist:

🧠 TL;DR

If you tried SwiftData before and ran into roadblocks — give it another shot in iOS 18 or 26. Model inheritance, better predicate support, and persistent history make it far more capable than before.

🏛 Core Data: The Veteran Still Holding Ground

Before SwiftData was even an idea, Core Data had been quietly (and sometimes loudly) powering iOS apps for over a decade. It’s battle-tested, deeply integrated into Apple platforms, and still very much alive in many production apps — especially at scale.

So while SwiftData is the new default for most new projects, there are still valid reasons to reach for Core Data in 2025.

🧠 What Is Core Data?

Core Data is Apple’s original object graph and persistence framework, introduced in iOS 3. It uses a more verbose, configuration-heavy setup, including:

But it’s extremely powerful, and it offers low-level control over things like:

✅ When Should You Still Use Core Data?

Consider Core Data if:

❌ Why Not Use Core Data (Anymore)?

📉 Example: The Difference

Core Data Save:

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let task = Task(context: context)
task.title = "Manual pain"
task.isDone = false
try? context.save()

SwiftData Save:

let task = Task(title: "Joyful code")
modelContext.insert(task)

Yeah. That’s the energy shift we’re talking about 😄

🏛 Core Data Setup: Full Overview for Legacy Projects

If you’re working in a codebase that still uses Core Data — or need advanced control not yet offered by SwiftData — here’s how to get it up and running properly.

🧱 Step 1: Create the Data Model File (MyModel.xcdatamodeld)

  1. In Xcode, right-click your project folder → New File
  2. Choose “Data Model” under Core Data
  3. Name it MyModel → Click Create
  4. Open the model editor and:

5\. Optionally, create relationships (e.g. between Task and Project)

✅ You’ve now defined your data schema visually.

🧰 Step 2: Set Up Core Data Stack in AppDelegate

class AppDelegate: UIResponder, UIApplicationDelegate {
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "MyModel") // matches your .xcdatamodeld file
        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Unresolved error: \(error)")
            }
        }
        return container
    }()
}

✅ This sets up your SQLite-backed Core Data store and makes it accessible via persistentContainer.viewContext.

🔧 Step 3: Generate Model Classes (Optional but Common)

  1. In .xcdatamodeld, select your entity (e.g. Task)
  2. From Xcode’s menu: Editor → Create NSManagedObject Subclass
  3. Xcode generates a class like this:
public class Task: NSManagedObject {
    @NSManaged public var title: String?
    @NSManaged public var isDone: Bool
}

✅ You can now use Task like any other Swift class — except it’s backed by Core Data.

🔧 Step 4: Core Data CRUD — All in One Code Block

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

// 1. Create
let task = Task(context: context)
task.title = "Legacy task"
task.isDone = false
try? context.save()

// 2. Read (Fetch All)
let request: NSFetchRequest<Task> = Task.fetchRequest()
let allTasks = try? context.fetch(request)

// 3. Read (Conditionally)
let pendingRequest: NSFetchRequest<Task> = Task.fetchRequest()
pendingRequest.predicate = NSPredicate(format: "isDone == NO")
let pendingTasks = try? context.fetch(pendingRequest)

// 4. Update
if let firstTask = allTasks?.first {
    firstTask.title = "Updated title"
    try? context.save()
}

// 5. Delete
if let toDelete = allTasks?.last {
    context.delete(toDelete)
    try? context.save()
}

🧠 Notes:

🧠 TL;DR

Core Data is still relevant, especially for large, existing, or enterprise apps. But for new SwiftUI-based apps in 2025, SwiftData is the better choice unless you absolutely need what Core Data uniquely offers.

🧭 Choosing the Right Tool: SwiftData vs Core Data vs AppStorage vs SceneStorage

With four different persistence tools in SwiftUI, it’s easy to wonder: “Which one should I use and when?” Here’s a straightforward guide to help you choose the right one for the right job — no fluff, just clarity.

✅ Use @AppStorage When:

Examples:

✅ Use @SceneStorage When:

Examples:

✅ Use SwiftData When:

Examples:

✅ Use Core Data When:

Examples:

🧠 TL;DR Cheat Sheet (Medium-Friendly)

⚠️ Common Pitfalls & Pro Tips (So You Don’t Pull Your Hair Out)

Even though SwiftData and friends aim to make persistence easier, there are still some sharp edges and subtle traps you should be aware of. Here’s a quick guide to what can go wrong, and how to stay out of trouble.

⚠️ 1. Don’t Overuse @AppStorage

Just because it’s easy doesn’t mean it’s always right.

Avoid:

Do:

⚠️ 2. @SceneStorage ≠ Long-Term Storage

@SceneStorage is only meant for UI-level state during the current session.

It does not:

Use it for:

⚠️ 3. SwiftData Objects Must Come From the Context

This one’s sneaky.

Problem: If you create an object outside the context or mutate a detached instance, changes won’t persist.

Fix: Always:

⚠️ 4. SwiftData Doesn’t Support CloudKit (Yet)

If your app needs iCloud sync between devices — Core Data is still your only option.

As of WWDC 2025:

⚠️ 5. Don’t Mix SwiftData and Core Data in One Project

Technically possible? Maybe. Recommended? Absolutely not.

Mixing the two leads to:

Pick one per project and stick with it. Use SwiftData if you’re starting fresh.

⚠️ 6. No Manual Save? Great, But Be Mindful

SwiftData saves changes automatically — but only when you’re modifying the actual tracked instance.

If you:

Then your changes might silently not persist.

✅ Wrapping Up: You Now Know SwiftUI Persistence Like a Pro

You’ve just walked through every major persistence tool SwiftUI offers — from the simplicity of @AppStorage to the power of SwiftData. We’ve covered how to create, fetch, update, delete, and choose wisely, so you’re not just copy-pasting — you actually understand what’s happening under the hood. 🧠💾

And with WWDC 2025 upgrades, SwiftData is no longer “the new kid” — it’s the tool most new SwiftUI apps should be using.

⏭ Next: Animations That’ll Make Your UI Shine ✨

In the next article in the SwiftUI Mastery Series:

_📌_ **SwiftUI Animations for Beginners: Learn with Simple Examples (2025 Edition)**

The next article in the SwiftUI Mastery Series covers animations for beginners — how withAnimation, .animation(), and transition() really work, with examples you can copy and play with.

If persistence was about brains, this one’s about beauty.

**SwiftUI Animations for Beginners: Learn with Simple Examples (2025 Edition)** _Animations in SwiftUI feel like magic 🪄 — until your view just snaps instead of fades. In this beginner-friendly…_medium.comhttps://medium.com/swift-pal/swiftui-animations-for-beginners-learn-with-simple-examples-2025-edition-76eb224eb633

🔗 More Articles you can read

If you’re ready to build more robust SwiftUI apps, here’s where you should go next:

Also, don’t forget to check out the full index of iOS articles here: 🔗 The Ultimate iOS Article Index by Karan Pal

**The Ultimate iOS Article Index by Karan Pal 🚀** _A growing collection of all my Swift and iOS development articles — neatly organized and updated regularly._medium.comhttps://medium.com/swift-pal/the-ultimate-ios-article-index-by-karan-pal-eb4fb5a42caf

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading