SwiftData
Summary of Steps
- Define your data structure (@Model): In this step, you create a regular Swift class to represent the data you want to store in your app. This class defines the properties (like title, description, etc.) of each data item.
- Tell SwiftData about your model (modelContainer): Here, you create a special container object that links your data model class to SwiftData. This lets SwiftData understand the structure of the data it will be managing.
- Make the data context accessible (Environment object): This step injects a special environment variable (
modelContext
) into your SwiftUI view. This variable acts as a bridge between your view and the data storage system. - Fetch data using
@Query
: The@Query
annotation is a shortcut for accessing your stored data. You simply add it in front of an array variable in your view, and SwiftData will automatically populate that array with the relevant data objects. - Manage your data (CRUD): This refers to the typical Create, Read, Update, and Delete operations you perform on your data. SwiftData provides methods on the
modelContext
to insert new items, fetch existing ones, update their properties, or remove them entirely.
Code:
- Define your data structure
SwiftData automatically handles the identification aspect of the model, eliminating the need for explicitly defining an
id
property or conforming to theIdentifiable
protocol.
// This class represents a single data item
@Model
final class Event {
// Properties
var name: String
// Class must have an init
init(name: String) {
self.name = name
}
}
2. Tell SwiftData about your model
// Create a container for your data model
@main
struct ExampleDataApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Event.self)
}
}
3. Make the data context accessible in SwiftUI
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// ... rest of your view code
}
4. Fetch data using @Query and bind to UI
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var events: [Event]
// ... rest of your view code
var body: some View {
List(events) { event in
Text(event.name)
}
}
}
5. Interact with your context for CRUD operations
@Environment(\.modelContext) private var modelContext
// Add a new event
func addEvent(){
let newEvent = Event("myevent")
modelContext.insert(newEvent)
}
// Update an existing event
func updateEvent(_ event: Event){
event.name = "new name"
try? modelContext.save()
input = ""
}
// Delete a event
private func deleteEvent(_ event: Event) {
modelContext.delete(event)
}
This is too easy … right?
Background
When we saw the LLVM we thought “now Apple can replace Objective-C”because they can develop a language that is just syntactic sugar on top of the LLVM.
And that is exactly what Swift is … leading to SwiftUI (!UIKit), Swift structured concurrency (!GCD) and now SwiftData (!CoreData???)
We always said that Apple will replace:
- Objective-C(rap)
- Storyboards
- UIKit
- GCD
- CoreData
We have been wait for this …
Objective-C(rap) is now considered dead.
We all need to learn Rust …
Teaching Swift structured concurrency.
The sooner you make the transition the less code you need to rewrite !!!
See the whole store here*:
*iOS is second part
We have been saying that CoreData is old tech and needs to be replaced. It feels disjoined from the way we build Swift/SwiftUI apps. Most developers agreed with us … and believed CoreData will be replace with a new “Swift CoreData” framework.
Well we are almost correct … SwiftData is not a replacement for CoreData but builds on top of it. But if CoreData is swapped out and everyone is using SwiftData we will never know the difference.
SwiftData fits perfect with:
- Swift
- SwiftUI
- Swift structured concurrency
- Interoperability with CoreData
Architecture
Before:
- [UI Layer — SwiftUI]
- [ViewModel]
- [Domain Layer — Use Cases — Business Logic]
- [Repository Layer]
- [Data Layer — CoreData]
Now:
- [UI Layer — SwiftUI]
- [Data / Domain Layer — Use Cases — Business Logic]
Using SwiftData changes the architecture of Apple (iOS, iWatch, Pad, Mac, visionOS) apps.
We went from building iOS apps in 3 weeks to 3 days …
The view is the ViewModel?:
Nice info from my friend Mohammad Azam:
SwiftData (Part1) : SwiftData You should watch all three parts.
If you try to use a ViewModel it gets complex:
Looking at the sample SwiftData app from WWDC23 it does not look like a MVVM app. Do not see a ViewModel anywhere …
Code: download here …
Nice SwiftData Guide from Mohammad Azam with an explanation of architecture.
Since 2019, I have used many different architectural patterns when building SwiftUI applications. This included MVVM, Container pattern, Redux, MV pattern and Active Record Pattern. Apple has a sample SwiftData application called Backyard Birds: Building an app with SwiftData and widgets, which uses a variation of Active Record Pattern.
Hacking with Swift (Paul Hudson) Why he does not use MVVM with SwiftData
“It works really badly with SwiftData, at least right now. This might improve in the future, but right now using SwiftData is basically impossible with MVVM.”
visionOS ViewModel — But the visionOS app has ViewModel everywhere for holding the current state of the window using the ObservableObject…
// View model
@MainActor class TimeForCubeViewModel: ObservableObject {
private let session = ARKitSession()
private let handTracking = HandTrackingProvider()
private let sceneReconstruction = SceneReconstructionProvider()
private var contentEntity = Entity()
private var meshEntities = [UUID: ModelEntity]()
private let fingerEntities: [HandAnchor.Chirality: ModelEntity] = [
.left: .createFingertip(),
.right: .createFingertip()
]
func setupContentEntity() { ... }
func runSession() async { ... }
func processHandUpdates() async { ... }
func processReconstructionUpdates() async { ... }
func addCube(tapLocation: SIMD3<Float>) { ... }
}
Not sure why they don’t use Observable …
This macro adds observation support to a custom type and conforms the type to the
Observable
protocol. For example, the following code applies theObservable
macro to the typeCar
making it observable:
@Observable
class Car {
var name: String = ""
var needsRepairs: Bool = false
init(name: String, needsRepairs: Bool = false) {
self.name = name
self.needsRepairs = needsRepairs
}
}
— — — —
Paul Hegarty @ Stanford always uses a ViewModel but he did not use SwiftData …
Should we use ViewModel or not 🤷🏼♀️ … Apple should give the answer.
With SwiftData we want to use the new SwiftUI state management
@State: Enables us to manage and mutate local state within a SwiftUI view.
@Bindable: A property wrapper that allows us to create custom bindings for data properties. Pass from parent to child. — replaces @Bind
@Environment: We can propagate values through the view hierarchy without passing them explicitly. — replaces @EnviormentVarable
Learning SwiftData
Building apps with SwiftData makes reactive programming super easy. Every part of the system is updated whenever anything changes without any extra programming.
WWDC has a great set of videos to get an introduction to SwiftData:
- Meet SwiftData
- Build an app with SwiftData
- Model your schema with SwiftData
- Discover Observation in SwiftUI
- Dive deeper into SwiftData
- Type-Safe Entity Models
- Automatic Migration
- Query Expressiveness
- Asynchronous Operations
- That being the case, using an in-memory
ModelConfiguration
is more suited for unit testing, or for supplying data to your Xcode previews.
Step 1: Create the model
Step 2: Create modelContainer
Step 3: Environment object for the context
Step 4: Add objects and Query to get the data
Step 5: Update objects, remove objects from or save the context
Steps for SwiftData
- @Model Macro — Mark a class to be persisted and observed
1.a Build the @Model — Build a schema for SwiftData
1.b Add all your properties for the model — @Attribute, @Relationship
1.c Setup relationships
2. Model Container — Connect the model to the app.
2.a Must register the model with the App — The modelContainer(for:)
modifier takes an array of types you want your model to track.
2.b Build the model container so we can load the model
3. Model Context — Simple SQL to CoreData
3.a Load the model Context form the Environment
3.b Use the context for all database (SQL) functions.
4. Fetching Data — Using SwiftData with SwiftUI
4.a Use @Query to fetch data when using SwiftUI
Extra. Discover Observation with SwiftUI
Deeper Dive
You need XCode15
Super easy to add it to your project.
SwiftData Development
- A wrapper for CoreData*
*We can move to something else besides CoreData and use the same API in the future if Apple decides CoreData (around for 15 years / released 2009) is not what they want to build on in the future.
Step1: @Model macro
@Model macro uses types you already know so nothing new to learn, and need minimal code to use @Model
- @Model macro
- Inferred or explicit structure
- Deep customization
@Model macro role — Central point of contact in apps that use SwiftData
- Describe the schema
- Instances used in code
Sets up reactive programming by configuring persistence
- Track and persist changes
- Modeling at scale
We use the @Model to connect this class to CoreData.
Note: Need an init()
Using the @Model macro provides
- Powerful new Swift macro
- Define your schema with code
- Add SwiftData functionality to model types
The model is the source of truth for your app. The class properties are stored in CoreData.
Using @Model macro provides:
- Attributes inferred from properties
- Support for basic value types — String, int and float
- Complex types : Struct, Enum, Codable, Collections of value types
Relationships are very powerful and can be:
- Other model types
- Collections of model types
Adding metadata
- @Model modifies all stored properties
- Control how properties are inferred using @Attribute, @Relationship, etc …
Schema for SwiftData
- Make it like a UUID — @Attribute(.unique)
- Update the name — @Attribute(originalName)
- Delete all associated data — @Relationship(.cascade)
@Relationship — Setup DB relationships between @Models
- originalName
- Specify toMany count constraints
- Hash modifier
@Transient — hide properties from SwiftData
- Specify which properties do not persist
- Provide a default value when not persisting values.
Simple Code:
Make the name unique, change the names of the dates, do not persist tripViews and cascade delete bucketList & livingAccommodation when this Trip is deleted!
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
@Relationship(.cascade)
var bucketList: [BucketListItem]? = []
@Relationship(.cascade)
var livingAccommodation: LivingAccommodation?
@Transient
var tripViews: Int = 0
}
Evolving Schemas (Overview):
- Encapsulate your models at a specific version with ‘VersionedSchema’
- Order your versions with SchemaMigrationPlan
- Define each migration state — Lightweight/Custom behavior
Next: Moving to step two … 👇🏾
Step2: Model container
In SwiftData, the concept of a “container” refers to the database
Model container provides you with a reactive framework
- Persistence backend
- Customized with configurations
- Provides schema migration options
Building a model container
- Sets up the model container
- Creates the storage stack
- Each View can only have a single model container
~~~ You can find out more about the model container including:
- Schema and persistence
- How objects are stored
- Evolution of models — Versioning, Migration, Graph separation
You can use the ModelConfiguration to
- Describes persistence of a schema — memory / disk
- Set the file location
- Make the model read only
Sample Code:
@main
struct TripsApp: App {
let fullSchema = Schema([
Trip.self,
BucketListItem.self,
LivingAccommodations.self,
Person.self,
Address.self
])
let trips = ModelConfiguration(
schema: Schema([
Trip.self,
BucketListItem.self,
LivingAccommodations.self
]),
url: URL(filePath: "/path/to/trip.store"),
cloudKitContainerIdentifier: "com.example.trips"
)
let people = ModelConfiguration(
schema: Schema([
Person.self,
Address.self
]),
url: URL(filePath: "/path/to/people.store"),
cloudKitContainerIdentifier: "com.example.people"
)
let container = try ModelContainer(for: fullSchema, trips, people)
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container) // array [] of models to use
}
}
Track and persist changes and perform changes in SwiftUI.
struct ContentView: View {
@Query var trips: [Trip] // changes managed by SwiftUI
@Environment(\.modelContext) var modelContext
var body: some View {
NavigationStack (path: $path) {
List(selection: $selection) {
ForEach(trips) { trip in
TripListItem(trip: trip)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
modelContext.delete(trip)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
.onDelete(perform: deleteTrips(at:))
}
}
}
}
ModelContext — tacks changes and updates the database
Step3: ModelContext
Functions the ModelContext provides
- Tracks objects to ModelContainer
- Propagates changes to ModelContainer
- Clear changes with rollback or reset
- Undo/redo support
- Autosave / Undo
- Automatically registers actions
- modelContainer() the enviroment’s undoManger
- Supports standard system gestures
Provides autosave-by default
- main context automatically saves
- System events
- Periodically as app is used
Provides a way to setup multiple containers
ModelContext — Environment variable
- Provides access to the model context
- View have single model context
Use the @Enviroment model context for
- Tracking updates
- Fetching models
- Saving changes
- Undoing changes
SwiftUI saves the changes without context.save! And supports SwiftData-backed document apps.
Get the model context from the environment after building your view.
Everything happens with the @Environment(\.modelContext)
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
var body: some View {
Get context for a model container or get a new context from the container.
Fetching your data with new Swift native types:
- Predicate — Fully type checked, #Predicate construction, Autocompleted keypaths
- FetchDescriptor
- SortDescriptor (improved)
- Relationships to prefetch
- Results limits
- Exclude unsaved changes
Build the predicate*
Query for a trip to “New York” on ‘birthday’ and the trip is in the future ….
*The ‘#’ in #Predicate and #Preview is the new Swift macro feature.
Step 4: Fetching Data
When using SwiftUI you can use @Query (See below)
FetchDescriptor
- Compiler validate queries
- Typed by model
- Additional parameters — Offset/limit and Faulting/prefetching
SortDescriptor
Use SortDescriptor to set the order
- Updated to support all Comparable types
- Swift native keypaths
With SwiftData we can do basic operations on data:
- Inserting
- Deleting
- Saving
- Changing
Use the model context to drive these operations
- context.insert(myData) — ready to use
- context.delete(myData) — call any of the above on your data
- context.save — Do not forget this step because nothing is done until save is called (unless called from SwiftUI).
@Model tracks and makes observations automatically
Using SwiftData with SwiftUI
SwiftData works on all platforms —visionOS, Mac, Pad, Phone, Watch
View modifiers
- Leverage scene and view modifiers
- Configure data store with .modelContainer
- Propagated throughout SwiftUI environment
Define:
@Model
final class Card {
var front: String
var back: String
var creationDate: Date
init(front: String, back: String, creationDate: Date = .now) {
self.front = front
self.back = back
self.creationDate = creationDate
}
}
New SwiftUI Data Management
SwiftUI Property wrappers:
@Observable
- Set up data flow with less code
- Automatic dependencies
- Seamlessly bind models’ mutable state to UI
Discover Observation with SwiftUI
- What is Observation:
When the model.addDonuts is called the model.donuts changes and the view will update.
@Observable class FoodTruckModel {
var orders: [Order] = []
var donuts = Donut.all
var orderCount: Int { orders.count }
}
struct DonutMenu: View {
let model: FoodTruckModel
var body: some View {
List {
Section("Donuts") {
ForEach(model.donuts) { donut in
Text(donut.name)
}
Button("Add new donut") {
model.addDonut()
}
}
Section("Orders") {
LabeledContent("Count", value: "\(model.orderCount)")
}
}
}
}
@State
When the view needs to have own state stored in the model, like the donutToAdd is needed to bind the data to the sheet.
The ‘donutToAdd’ property is managed by the lifetime of the view it’s contained in.
struct DonutListView: View {
var donutList: DonutList
@State private var donutToAdd: Donut?
var body: some View {
List(donutList.donuts) { DonutView(donut: $0) }
Button("Add Donut") { donutToAdd = Donut() }
.sheet(item: $donutToAdd) {
TextField("Name", text: $donutToAdd.name)
Button("Save") {
donutList.donuts.append(donutToAdd)
donutToAdd = nil
}
Button("Cancel") { donutToAdd = nil }
}
}
}
- ObservableObject — How to update
@Environment
Environment lets values be propagated as globally accessible values and shared in many places.
Example: When the userName changes the View updates
@Observable class Account {
var userName: String?
}
struct FoodTruckMenuView : View {
@Environment(Account.self) var account
var body: some View {
if let name = account.userName {
HStack { Text(name); Button("Log out") { account.logOut() } }
} else {
Button("Login") { account.showLogin() }
}
}
}
@Bindable
- Lightweight
- Connect references to UI
- Uses $ syntax to create bindings
Example: TextField takes a binding and updates the both reading and updating the field.
@Observable class Donut {
var name: String
}
struct DonutView: View {
@Bindable var donut: Donut
var body: some View {
TextField("Name", text: $donut.name)
}
}
Know the difference
When to use what?
Computed properties
- SwiftUI tracks access
- Composed properties
- Manual control when needed
Converting code is very easy with only three options.
Query
@Qurey property wrapper
- Provides the view with data
- Triggers view update on every change of the models
- A view can have multiple @Query properties
- filter, order and animate changes.
- Uses ModelContext as the source of data
Load and filter anything in your DB with a single line of code.
SwiftUI will automatically refresh any observed changes !!!
Examples:
Now we can Query the data
@Query private var items: [Item]
How to build a Query
@MainActor
@propertyWrapper
struct Query<Element, Result> where Element : PersistentModel,
Element == Result.Element, Result : Collection
From DocC how Query works.
/// Create a query with a predicate, a key path to a property for sorting,
/// and the order to sort by.
///
/// Use `Query` within a view by wrapping the variable for the query's
/// result:
///
/// struct RecipeList: View {
/// // Recipes sorted by date of creation
/// @Query(sort: \.dateCreated)
/// var favoriteRecipes: [Recipe]
///
/// var body: some View {
/// List(favoriteRecipes) { RecipeDetails($0) }
/// }
/// }
///
/// - Parameters:
/// - filter: A predicate on `Element`
/// - sort: Key path to property used for sorting.
/// - order: Whether to sort in forward or reverse order.
/// - animation: The animation to use for user interface changes that
/// result from changes to the fetched results.
Not much documentation all the different ways to use @Query but keep checking back as we build out this document.
SwiftUI Preview
To make SwiftUI Preview working with SwiftData.
Using the code from:
The BudgetListScreen()
View has SwiftData:
- @Query
- @Environment ( \.modelContext)
that makes rendering the preview different …
struct BudgetListScreen: View {
@Query private var budgets: [Budget]
@Environment(\.modelContext) private var context
@State private var name: String = ""
@State private var limit: Double? = nil
private var isFormValid: Bool {
!name.isEmptyOrWhiteSpace && limit != nil
}
You have to setup the preview with the `modelContainer(for: Budget.self)`
#Preview {
NavigationStack {
BudgetListScreen()
.modelContainer(for: Budget.self)
}
}
_____
And the preview gets more complex when the model has relationships.
struct BudgetDetailScreen: View {
let budget: Budget // This relatoinship will not allow the preview to work
@State private var note: String = ""
@State private var amount: Double?
@State private var date: Date = Date()
@State private var hasReceipt: Bool = false
private func saveTransaction() {
let transaction = Transaction(note: note, amount: amount!, date: date, hasReceipt: hasReceipt)
// belongsTo to relationship
transaction.budget = budget
// hasMany relationship
budget.addTransaction(transaction)
}
...
Create some data for the modelContainer in the preview directory.
import Foundation
import SwiftData
@MainActor // using the MainActor for threading
let previewContainer: ModelContainer = {
do {
let container = try ModelContainer(for: Budget.self, ModelConfiguration(inMemory: true))
// create some budgets
SampleData.budgets.enumerated().forEach { index, budget in
container.mainContext.insert(budget) // Import the budget is inserted into the context
// transaction for each budget
let transaction = Transaction(note: "Note \(index + 1)", amount: (Double(index) * 10), date: Date())
transaction.budget = budget
budget.addTransaction(transaction)
}
return container
} catch {
fatalError("Failed to create container.")
}
}()
struct SampleData {
static let budgets: [Budget] = {
return (1...5).map { Budget(name: "Budget \($0)", limit: 100 * Double($0)) }
}()
}
You can NOT just pass that into the Preview. It will not even compile!
#Preview {
BudgetDetailScreen(budget: previewContainer) // will not compile
}
With the sample data you created you can make a new container view just for the preview :-)
struct BudgetDetailContainerView: View {
@Query private var budgets: [Budget]
var body: some View {
NavigationStack {
BudgetDetailScreen(budget: budgets[0])
}
}
}
Now you can use the preview but only on the MainActor
#Preview { @MainActor in
BudgetDetailContainerView()
.modelContainer(previewContainer) // we use the previewContainer
}
Pre-populate a SwiftData Persistent Store
You need the appContainer
declaration with the @MainActor
attribute because container
’s mainContext
is annotated with the @MainActor
attribute. You’ll get a compiler error like this if you don’t:
Main actor-isolated property ‘mainContext’ can not be referenced from a non-isolated context
Populate XCode preview
@MainActor
let previewContainer: ModelContainer = {
do {
let container = try ModelContainer(
for: Item.self, ModelConfiguration(inMemory: true)
)
let items = [
Item(timestamp: Date()),
Item(timestamp: Date()),
Item(timestamp: Date())
]
for item in items {
container.mainContext.insert(object: item)
}
return container
} catch {
fatalError("Failed to create container")
}
}()
And that is how to do Previews with SwiftData.
Unit Test
Writing Unit Tests for SwiftData Domain Logic.
This is video Mohammad Azam shows how to use a mock container to test the SwiftData.
@MainActor
is a global actor that uses the main queue for executing its work.
The test:
Pleases watch the full video here (and give a 👍🏼)!!!
More to come as Apple releases more info …
Best,
~Ash