Modelling Schema with SwiftData: A Comprehensive Guide

Gerald Brigen
4 min readJul 6, 2023

--

Introduced at WWDC23, SwiftData is a powerful and expressive persistence framework built for Swift. It allows developers to model their data directly from Swift code, work with their models using SwiftData, and integrate with SwiftUI. This article will guide you through the process of using SwiftData, complete with a step-by-step example.

Defining the Model

To define a schema with SwiftData, you need to import the SwiftData framework and add the @Model annotation to your model class. The types can be value types, enums, structs, codable, or collections of the value types.

Here’s an example:

import SwiftData

@Model struct Trip {
@Attribute(.unique) var id: UUID
@Attribute var name: String
@Attribute var destination: String
@Attribute var startDate: Date
}

In this example, @Attribute(.unique) is used to enforce a uniqueness constraint on the id attribute. The @Attribute annotation is used to define the properties of the model that will be stored in the database.

Creating the Model Container

The model container provides the persistent backend. You create a container by specifying the list of model types to store. Optionally, you can provide a ModelConfiguration with a URL, CloudKit and group container identifiers, and migration options.

let container = try ModelContainer(for: [Trip.self], configurations: ModelConfiguration(url: URL("path")))

In SwiftUI, you can use the modifier .modelContainer(for: [Trip.self]).

Using ModelContext

ModelContext is the interface for tracking updates, fetching data, saving changes, and even undoing those changes. In SwiftUI, you can use @Environment(\\.modelContext) private var context. For a shared MainActor bound context, you can use container.mainContext.

Fetching Data

To fetch data, you can use Swift native types Predicate and FetchDescriptor. Here’s an example:

let today = Date()
let tripPredicate = #Predicate<Trip> {
$0.destination == "New York" &&
$0.name.contains("birthday") &&
$0.startDate > today
}
let descriptor = FetchDescriptor<Trip>(
sortBy: SortDescriptor(\\Trip.name),
predicate: tripPredicate
)
let trips = try context.fetch(descriptor)

Modifying Data

To modify data, you can use the insert and delete methods of the context. Here’s an example:

var myTrip = Trip(name: "Birthday Trip", destination: "New York")

// Insert a new trip
context.insert(myTrip)

// Delete an existing trip
context.delete(myTrip)

// Manually save changes to the context
try context.save()

The @Model macro modifies setters for change tracking and observation.

SwiftData in SwiftUI

SwiftData can be easily integrated with SwiftUI. You can configure the data store with .modelContainer which is propagated throughout the SwiftUI environment. Fetching in SwiftUI is done with @Query. There’s no need for @Published and SwiftUI automatically updates.

import SwiftUI

struct ContentView: View {
@Query(sort: \\.startDate, order: .reverse) var trips: [Trip]
@Environment(\\.modelContext) var modelContext

var body: some View {
NavigationStack() {
List {
ForEach(trips) { trip in
// ...
}
}
}
}
}

In conclusion, SwiftData offers a powerful and expressive way to handle data persistence in Swift.

Building an App with SwiftData

Now that we’ve covered the basics of SwiftData, let’s dive into a practical example of building a multi-platform SwiftUI app with SwiftData. This will involve converting existing model classes into SwiftData models, setting up the environment, reflecting model layer changes in the UI, and building document-based applications backed by SwiftData storage.

Setting Up Data Flow with @Observable

SwiftData allows you to set up data flow with less code using the @Observable property wrapper. This enables automatic dependencies and seamlessly binds your models’ mutable state to the UI.

Fetching Data with @Query

The @Query property wrapper provides the view with data and triggers a view update on every change of the models. A view can have multiple @Query properties. @Query uses ModelContext as the source of data. Here’s an example:

@Query(sort: \\.created) private var cards: [Card]

@Query needs a model context to work. We get the model context from the model container.

Setting Up the Model Container with .modelContainer

The .modelContainer view modifier sets up the model container and creates the storage stack. A view has a single model container, and an app needs to set up at least one model container. Model containers are inherited by the views (like .environment()). Different windows (or views inside windows) can have different model containers. Here’s an example:

.modelContainer(for: Card.self)

Accessing the Model Context with @Environment

The @Environment(\\.modelContext) provides access to the model context. A view has a single model context, but an application can have multiple. This environment variable is populated automatically when we set the model container earlier. Here’s an example:

@Environment(\\.modelContext) private var modelContext

Saving the Context

You might think that after inserting the model, you need to save the context by calling modelContext.save(), but you don’t need to do that. A nice detail about SwiftData is that it autosaves the model context. The autosaves are triggered by UI-related events and user input. We don’t need to worry about saving because SwiftData does it for us. There are only a few cases when you want to make sure that all the changes are persisted immediately, for example, before sharing the SwiftData storage or sending it over. In these cases, call save() explicitly.

Building Document-Based Applications with DocumentGroup

On iOS and macOS, instead of the WindowGroup, we can use the DocumentGroup. This makes the app a document-based app. You have to set up a custom contentType/file format for your documents. The content type has to be set up in Info.plist with the same identifier as you use in code. The content type set up in Info.plist needs to conform to com.apple.package. You should NOT set up a .modelContainer for the DocumentGroup, as it does it automatically.

With these steps, you can build a robust, multi-platform SwiftUI app using SwiftData. The power of SwiftData lies in its seamless integration with SwiftUI and its ability to handle data persistence in a Swift-native way.

--

--