SwiftData

The New Way (Architecture) Forward

Siamak (Ash) Ashrafi
17 min readJul 21, 2023

Summary of Steps

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

  1. 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 the Identifiable 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 …

Teaching Swift as soon as released
Teaching Swift @ AT&T Dev Fest
Moving people from UIKit to SwiftUI & Combine Framework
Moving from UIKit to SwiftUI

Objective-C(rap) is now considered dead.

JebBrains State of Development EcoSystem

We all need to learn Rust …

Teaching Swift structured concurrency.

“Liked” by the man himself Chris Lattner
Teaching Swift structured concurrency as soon as released
Combine Framework & Combine Framework
Teaching Swift structured concurrency

The sooner you make the transition the less code you need to rewrite !!!

Please leave a comment if you would like help.

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:

  1. [UI Layer — SwiftUI]
  2. [ViewModel]
  3. [Domain Layer — Use Cases — Business Logic]
  4. [Repository Layer]
  5. [Data Layer — CoreData]

Now:

  1. [UI Layer — SwiftUI]
  2. [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:

SwiftUI does not need a ViewModel

SwiftData (Part1) : SwiftData You should watch all three parts.

Learn SwiftData

If you try to use a ViewModel it gets complex:

Does SwiftData with MVVM make sense?

Looking at the sample SwiftData app from WWDC23 it does not look like a MVVM app. Do not see a ViewModel anywhere …

Running WWDC23 SwiftData code example

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.”

Azam Sharp post on Paul Hudson post about MVVM and SwiftData

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 the Observable macro to the type Car 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:

  1. Type-Safe Entity Models
  2. Automatic Migration
  3. Query Expressiveness
  4. Asynchronous Operations
  5. 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

  1. @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

SwiftData 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
Relationship between ModelContainer and ModelContext

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.

Mock Container

@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

--

--