SwiftData: Simplifying Persistence in iOS Apps
Learn all about SwiftData, a new framework introduced at WWDC 2023 that provides a Swift-like API for working with persistence in iOS apps and simplifies Core Data usage. By Josh Steele.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SwiftData: Simplifying Persistence in iOS Apps
10 mins
It’s the dawn of a new day for dealing with persisting data in your iOS apps.
SwiftData is here.
OK, hyperbole aside, users of Core Data have waited for SwiftData for a long time. What is SwiftData, and why should you care?
What Is SwiftData?
Before diving into some of the details of SwiftData, you need to know what it is. SwiftData was introduced at WWDC 2023 and is a framework that provides a Swift-like API for working with persistence in your app. You might even say it’s “Swift native”.
An important distinction to make here is that SwiftData still uses the underlying storage architecture of Core Data. SwiftData simply presents a more user-friendly syntax for working with Core Data.
Actually, “simply” is a poor choice of words. If you’ve worked with Core Data in the past, you’ll find SwiftData’s new syntax simply amazing.
To understand why it’s so amazing, a small look back is required.
A Brief Look Back
Ever since Swift came out, using Core Data with your app has always seemed out of place. All of the “Swift-y” features that came out each year with Swift and SwiftUI were leaving Core Data, which had a deep Objective C heritage, in the dust.
A good example here is the .xcdatamodeld, or Schema Model Editor, file. This file is used to define your database’s schema.
This is a convenient way to define all the elements of your model, but it feels separate from the rest of your code. In fact, the compiler uses the schema to make class files for you, but they’re located in the derived data of your project! This technique also differs from the approach taken in SwiftUI, which pushes developers toward defining everything in code instead of separate helper files like storyboards.
Incremental Changes
This isn’t to say that Apple was ignoring Core Data. Each WWDC would see some welcome improvements to the framework. The creation of the NSPersistentCloudKitContainer
encapsulated a large chunk of code that developers normally had to write themselves to keep their Core Data and CloudKit stores in sync. The introduction of property wrappers such as @FetchRequest
and @SectionedFetchRequest
helped keep SwiftUI views in sync with the database just like a normal @State/@Binding pair. In fact, property wrappers gave a lot of people hope that something could be done to make Core Data a bit more “Swift-y”.
Then Swift 5.9 was released.
Swift Macros and Swift Data
The introduction of Swift macros in Swift 5.9 looks like it’ll be a game changer. There’s sure to be a lot of content here at Kodeco to cover Swift macros in the near future, so for now, here are some of the highlights while checking out SwiftData.
Here’s a model for a Recipe
class:
class Recipe {
var name: String
var summary: String?
var ingredients: [Ingredient]
}
If I were using Core Data, I’d have to go into the Schema Editor, add a new entity, and add attributes for the properties. With SwiftData, that’s all done with one addition, @Model
:
import SwiftData
@Model
class Recipe {
var name: String
var summary: String?
var ingredients: [Ingredient]
}
That’s it! Our Recipe
class is now a valid model for use in SwiftData, which has its own import when you want to use it. But what exactly is @Model
? Right-clicking on @Macro
and choosing Expand Macro shows exactly what this macro has added to your class:
That’s a lot of added code! The @Model
macro sets up a perfectly valid model, but you can also make customizations. For example, to ensure the name is unique, you can add a macro to that property:
@Model
class Recipe {
@Attribute(.unique) var name: String
var summary: String?
var ingredients: [Ingredient]
}
You can even define deletion rules for the relationships using the @Relationship
macro:
@Model
class Recipe {
@Attribute(.unique) var name: String
var summary: String?
@Relationship(.cascade)
var ingredients: [Ingredient]
}
Associating the Model With Your App
Gone are the days of the Persistence.swift file for initializing the persistence stack for your app. SwiftData has a new modifier that lets you define exactly which types you want to consider part of your model:
@main
struct RecipeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Recipe.self, Ingredient.self])
}
}
The modelContainer(for:)
modifier takes an array of types you want your model to track.
That’s it! There’s no step 2! But what about accessing the data?
Accessing Data
With a model defined and the modelContainer
injected into the environment, you can access your database entries!
@Query var recipes: [Recipe]
var body: some View {
List(recipes) { recipe in
NavigationLink(recipe.name, destination: RecipeView(recipe))
}
}
That’s it! There’s still no step 2! You can, however, customize the query to handle things like sorting:
@Query(sort: \Recipe.name, order: .forward)
var recipes: [Recipe]
var body: some View {
List(recipes) { recipe in
NavigationLink(recipe.name, destination: RecipeView(recipe))
}
}
Inserting and Deleting Data
To insert and delete data from the datastore in Core Data, you needed access to the store’s context. The same is true for SwiftData. When you set up the .modelContainer
earlier, that also set up a default model context and injected it into the environment. This allows all SwiftUI views in the hierarchy to access it via the \.modelContext
key path in the environment.
Once you have that, you can use context.insert()
and context.delete()
calls to insert and delete objects from the context.
struct RecipesView: View
{
@Environment(\.modelContext) private var modelContext
@Query(sort: \Recipe.name, order: .forward)
var recipes: [Recipe]
var body: some View {
NavigationView {
List {
ForEach(recipes) { recipe in
NavigationLink(recipe.name, destination: RecipeView(recipe))
}
.onDelete(perform: deleteRecipes)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addRecipe) {
Label("Add Recipe", systemImage: "plus")
}
}
}
}
private func addRecipe() {
withAnimation {
let newRecipe = Recipe("New Recipe")
modelContext.insert(newRecipe)
}
}
private func deleteRecipes(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(recipes[index])
}
}
}
}
If you’ve used Core Data in the past, you may have noticed there’s no call to context.save()
. That’s right — because it’s no longer required. By default, SwiftData will autosave your context to the store on a state change in the UI or after a certain time period. You’re free to call save()
if you wish, though.