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 —…

🔰 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:
- AppStorage for saving simple user settings
- SceneStorage for keeping per-view UI state
- Core Data for complex object graphs and legacy projects
- And now, the shiny new SwiftData — a Swift-native framework designed to make persistence feel like a natural part of your SwiftUI workflow
💡 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:
- Automatically loads the stored value at launch
- Saves any new value the moment the variable changes
- Keeps the view updated whenever the value changes
✅ When Should You Use It?
Use @AppStorage when:
- You want to persist a small, app-wide setting
- You need to survive app terminations and relaunches
- You’re saving simple values, like: \- Dark mode preference \- App language \- HasSeenOnboarding flag \- Last selected tab
🧪 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:
- On first launch, if the key “isDarkMode” doesn’t exist in UserDefaults, the default (false) is used.
- If the user toggles it to true, SwiftUI:
- Updates the view reactively
- Stores true in UserDefaults behind the scenes
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
- 🧠 Default values only apply the first time (when nothing is stored).
- 🎯 Ideal for user preferences, toggles, flags — things that don’t need relationships or structure.
- ☁️ Works with iCloud syncing if UserDefaults is set up for shared containers.
- ❌ Not designed for storing custom objects or complex models. Stick to basic types.
🗂️ @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?
- Restoring scroll position or navigation stack
- Remembering the selected tab in a
TabView - Keeping track of what was typed in a text field
- Basically: any transient view state that you want to auto-restore
🧪 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
- The value is stored per scene, not globally.
- It’s lost if the scene is fully destroyed (like a manual app kill or memory pressure closure).
- Unlike
@AppStorage, it doesn’t persist across full app terminations or device restarts.
🔥 Pro Tips
- Great for non-critical UI state like scroll position, tab selection, drafts.
- ❌ Don’t rely on it for permanent storage — use
@AppStorageor SwiftData for that. - It works best when paired with system scenes (iPad multi-window, macOS apps, etc.)
🧱 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:
- A Swift-native, code-first alternative to Core Data
- Built around Swift macros like
@Model,@Query, and@Relationship - Designed for automatic syncing with SwiftUI views via
@Environment(\.modelContext) - A framework that uses SQLite behind the scenes, but abstracts it completely
In simpler terms: it gives you real persistence with real Swift syntax and real joy instead of frustration.
🪄 What Makes It Special?
- Fully Swift-native syntax (no more Xcode model editor!)
- Reactive and observable — changes trigger SwiftUI updates
- Async-friendly and built for modern concurrency
- Model-first design using
@Modelmacro
✍️ 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)
}- ✅ No save call
- ✅ SwiftData persists it automatically
- ✅ Triggers UI updates where
@Queryor@Bindableis used
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
- Create → Just call
modelContext.insert(myObject). No save button, no ceremony. - Read (All) → Use
@Queryfor live updates in views, or fetch manually withmodelContext.fetch(). - Read (Filtered) → Pass a
#PredicateinsideFetchDescriptorto filter results by any property (e.g., title, isDone, date). - Update → Directly mutate your model’s properties. SwiftData observes and saves automatically.
- Delete → Call
modelContext.delete(object)— simple as that.
⚠️ Gotchas and Things to Know
- SwiftData uses SQLite under the hood (stored on-disk by default)
- All models share a single context unless manually configured
- No need to create a DB file — SwiftData handles it
- As of WWDC 2025, features like model inheritance, predicate support for enums/Codable, and persistent history tracking have all been added (we’ll cover that in the next section)
- CloudKit sync and Core Data migration tooling are still missing
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:
- Undo/redo operations
- Change tracking
- Synchronization across views or even scenes (think iPad multi-window)
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:
- Crashes when using
@ModelActorare now resolved - View update bugs during async mutations are gone
- Schema generation and relationship syncing are much more predictable
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:
- ❌ No support for CloudKit sync or sharing
- ❌ No official Core Data → SwiftData migration tool
- ❌ No advanced indexing or full-text search
- ❌ Relationship predicates are still limited (e.g., deep filtering)
🧠 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:
NSManagedObjectModelNSPersistentContainerNSManagedObjectContext- And lots of explicit save/fetch boilerplate
But it’s extremely powerful, and it offers low-level control over things like:
- Fetch performance tuning
- Migration and versioning
- Indexed properties
- Batch updates and deletes
- iCloud/CloudKit sync (yes, SwiftData still doesn’t have this!)
✅ When Should You Still Use Core Data?
Consider Core Data if:
- You’re maintaining or expanding a legacy app
- You need fine-grained control over the database
- You require CloudKit syncing or advanced migrations
- You’re building a data-heavy enterprise app with large schemas, performance tuning, or migration history
❌ Why Not Use Core Data (Anymore)?
- Steep learning curve
- Verbose boilerplate and config
- Not friendly with SwiftUI (without wrappers or extra work)
- More code just to do what SwiftData does in a few lines
📉 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)
- In Xcode, right-click your project folder → New File
- Choose “Data Model” under Core Data
- Name it MyModel → Click Create
- Open the model editor and:
- Add an Entity (e.g.
Task) - Add attributes like
title: StringandisDone: Bool
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)
- In
.xcdatamodeld, select your entity (e.g.Task) - From Xcode’s menu: Editor → Create NSManagedObject Subclass
- 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:
Taskhere refers to a Core Data entity you've created in.xcdatamodeld- Always wrap
try?in properdo-catchblocks in production - You can use
NSPredicateandNSSortDescriptorjust like SwiftData, but it’s more verbose
🧠 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:
- You want to store simple user preferences
- Values are global and don’t need structure
- You need persistence across app launches
- Types are
Bool,String,Int,Double, etc.
Examples:
- Dark mode toggle
- “Has seen onboarding” flag
- Preferred language
✅ Use @SceneStorage When:
- You need to store transient view state like:
- Tab selection
- Navigation path
- Draft text
- You want automatic restoration after suspending the app or switching scenes
- You don’t need it to persist across full app termination
Examples:
- Remembering the last selected tab on iPad
- Keeping scroll position after switching apps
✅ Use SwiftData When:
- You need to store structured, relational data
- You want a clean, Swift-native persistence layer
- You’re working with SwiftUI
- You want automatic view updates, reactive behavior, and async support
- You’re building a new app in 2025
Examples:
- To-do lists
- Notes with categories
- Blog posts, users, comments
✅ Use Core Data When:
- You’re maintaining a legacy app
- You require CloudKit sync, batch updates, or custom migrations
- You’re working on a large-scale enterprise app
- You need fine-tuned fetch performance
Examples:
- Existing corporate projects
- Highly customized data models with long-term schema history
🧠 TL;DR Cheat Sheet (Medium-Friendly)
- Simple flag or setting? →
@AppStorage - View state that disappears when the app closes? →
@SceneStorage - Modern, structured data for new SwiftUI apps? →
SwiftData - Need CloudKit or stuck in legacy land? →
Core Data
⚠️ 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:
- Storing complex objects or large data blobs in
@AppStorage - Using it for every small state variable (leads to cluttered UserDefaults)
Do:
- Limit it to small, global flags or settings
- Use SwiftData for anything relational or structured
⚠️ 2. @SceneStorage ≠ Long-Term Storage
@SceneStorage is only meant for UI-level state during the current session.
It does not:
- Survive full app kills
- Replace real data persistence
Use it for:
- Navigation state
- Scroll positions
- Form input in-progress
⚠️ 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:
- Use
@Queryto fetch - Pass objects using
@Bindableor@ObservedObject - Avoid creating “offline” copies of models
⚠️ 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:
- ✅ Persistent history is supported
- ❌ CloudKit syncing is not
- ❌ Migration from Core Data is not automated
⚠️ 5. Don’t Mix SwiftData and Core Data in One Project
Technically possible? Maybe. Recommended? Absolutely not.
Mixing the two leads to:
- Conflicting data layers
- Multiple storage systems
- Confusing developer experience
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:
- Copy a model
- Modify a detached reference
- Pass data in/out of a Task or async closure without proper binding
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:
- 💡 MVVM in SwiftUI: Build Scalable & Testable Apps
- 🧪 Unit Testing: Test ViewModels, Views, and Flows
- ⚙️ SwiftUI with Combine: Real-Time Data Explained
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! 🚀
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
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read