As an iOS developer, managing and persisting your application’s data is a crucial task, and today, I’ll take you through the core components of the SwiftData stack – the Schema, Container, and Context – and explain how they work in harmony to take much of this load off your shoulders. The SwiftData stack is very similar to the CoreData stack but there are some important differences. If you have a CoreData project read How to convert a CoreData project to SwiftData.
I will mainly focus on the ModelContainer and its configurations. This also includes how to use iCloud sync with SwiftData. More importantly, I’ll show you how to use these components in your SwiftUI projects.
⬇️ download the project from GitHub https://github.com/gahntpo/SnippetBox-SwiftData
Requirement: Xcode 15
What is the SwiftData Stack
The SwiftData stack refers to the collection of framework classes that work together to provide the object graph management and persistence capabilities of the SwiftData framework. It consists of four main components:
- Model Classes: The model classes define the schema for your SwiftData application. It defines your data model’s entities (or tables), including their attributes, relationships, and constraints.
- ModelContainer: The model container is the broker between the model context and the persistent store (or stores). It creates a database from a given schema. It manages the reading and writing of data to and from disk and it also can handle iCloud sync.
- ModelContext: The context is like a working scratch pad for your application. When you fetch, create, or delete managed objects, you do so in the context. It’s also the object that tracks changes to model objects and offers methods for saving those changes to the container, which saves it to the underlying database.
- Database: This is the actual physical storage file. SwiftData uses an SQLite database. For more options, you can look at Core Data which can also use XML, binary files, or in-memory stores. With both CoreData and SwiftData, you never have to directly talk to the database. This is all done for you by the container.
The objects in this stack collaborate to handle much of the work of managing and persisting your application’s data, allowing you to focus on the unique functionality of your application. In the following sections, I will show you how to use these components in your SwiftUI projects.
Creating A SwiftData Schema and Understanding SwiftData’s Schema Macros
Defining a data model in SwiftData involves creating a schema, a blueprint that outlines how data should be organized and accessed. Unlike other frameworks that require separate files or tools for schema definition, SwiftData leverages Swift’s capabilities to allow schema definition directly in your code, using classes and Swift macros.
One of the essential macros in SwiftData is the @Model macro. This macro informs SwiftData that a class should be treated as a model, meaning that it should have a corresponding schema in the database. Here’s a simple example:
@Model final class Snippet {
var title: String
var code: String
var folder: Folder?
init(title: String, code: String, folder: Folder? = nil) {
self.title = title
self.code = code
self.folder = folder
}
}
@Model final public class Folder {
var title: String
@Relationship(.cascade, inverse: \Snippet.folder) var snippets: [Snippet]
init(title: String) {
self.title = title
}
}
In this example, SwiftData will automatically create a schema for the Snippet
and Folder
models including the one-to-many relationship.
SwiftData also enhances the model by automatically conferring it the PersistentModel
conformance. This means that your model is now Identifiable
, Observable
, and Hashable
. The Observable
protocol is especially useful because it ensures live updates when the model is used in SwiftUI views.
Beyond the @Model macro, SwiftData provides additional macros like @Attribute
and @Relationship
to fine-tune your schema definition. The @Attribute
macro allows you to add constraints to properties, like making them unique or renaming them. The @Relationship
macro, on the other hand, helps you define relationships between different models and set delete rules.
SwiftData schemas are flexible and customizable, allowing you to adjust them to suit your specific needs. With SwiftData, you get a simplified, streamlined approach to data modeling in Swift, making it a powerful tool for data persistence and management. For a detailed breakdown of defining a schema have a look at this blog post: Modeling Data in SwiftData.
Working with the ModelContainer in SwiftData
You can create a ModelContainer from your model classes. You need to specify the model class, so that the container can create the corresponding database from it. Here is how you can create a container instance:
let container = ModelContainer(for: [Snippet.self, Folder.self])
If you have models that have relationships with each other, you only need to specify one model class and the container will infer the related model classes. In the above example, there is a one-to-many relationship between folder and snippet, which means I only need to specify:
let container = ModelContainer(for: Snippet.self)
Configuring a ModelContainer
You can specify how your container is configured with the ´ModelConfiguration´. For example, setting up a temporary SwiftData store can be done with the following configuration:
let schema = Schema([Folder.self, Snippet.self, Tag.self])
let configuration = ModelConfiguration(inMemory: true)
let container = try! ModelContainer(for: schema, configurations: [configuration])
Here is the full list of options for the ModelConfiguration:
ModelConfiguration(
_ name: String? = nil,
schema: Schema? = nil,
inMemory: Bool = false,
readOnly: Bool = false,
sharedAppContainerIdentifier: String? = nil,
cloudKitContainerIdentifier: String? = nil
)
Another example would be to add a default database in the apps bundle and use this to show users example data. Here is how you would get this container:
let schema = Schema([Folder.self, Snippet.self, Tag.self])
let configuration = ModelConfiguration(url: "myBundelURL", inMemory: true)
let container = try! ModelContainer(for: schema, configurations: [configuration])
Getting the File Location of a SwiftData Store
SwiftData will create a database file when the app first launches. You can get the location of the file that is located in the support directory like so:
guard let urlApp = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last else { return }
let url = urlApp.appendingPathComponent("default.store")
if FileManager.default.fileExists(atPath: url.path) {
print("swiftdata db at \(url.absoluteString)")
}
The file is of type .store. It uses an SQLite database underneath.
Using iCloud Sync with SwiftData
You can sync your user’s data across devices with SwiftData. This only works for a private CloudKit database. If you want to use a shared or public database you have to fall back to CoreData.
In SwiftData add iCloud sync to your app’s entitlements:
You can then use your container identifier for iCloud to setup a new SwiftData container:
let schema = Schema([Folder.self, Snippet.self, Tag.self])
let configuration = ModelConfiguration(cloudKitContainerIdentifier: "iCloud.com.karinprater.snippetbox")
let container = try! ModelContainer(for: schema, configurations: [configuration])
Note that the same model restrictions as for CoreData with iCloud sync are also valid: unique constraints are not supported and all relationships have to be optional.
Setting a Migration Plan for ModelContainer
You can set up a migration plan with SwiftData. This is explained in the WWDC presentation Model your schema with SwiftData.
Setting up a ModelContainer in a SwiftUI Application
SwiftUI has a convenient modifier to create a model container. In the main app file, add a modelContainer
modifier:
import SwiftUI
import SwiftData
@main
struct SnippetBoxApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Snippet.self)
}
}
}
This will create a model container and pass the main model context of this container in the environment. You can access the context in any view like so:
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var context
var body: some View {
...
}
}
The container will only be created once. There is only one instance. You cannot change any of the configuration parameters.
func modelContainer(
for modelType: PersistentModel.Type,
inMemory: Bool = false,
isAutosaveEnabled: Bool = true,
isUndoEnabled: Bool = false,
onSetup: @escaping (Result<ModelContainer, Error>) -> Void = { _ in }
) -> some View
You can create and pass a configured container in SwiftUI. The following is a simple example, where I set the cloud kit container from above:
import SwiftUI
import SwiftData
@main
struct SnippetBoxApp: App {
var container: ModelContainer? = {
let conf = ModelConfiguration(cloudKitContainerIdentifier: "iCloud.com.karinprater.snippetbox")
do {
let container = try ModelContainer(for: Snippet.self, conf)
return container
} catch {
print("errror: \(error)")
return nil
}
}()
var body: some Scene {
WindowGroup {
if let container = container {
ContentView()
// .modelContainer(for: Snippet.self)
.modelContainer(container)
} else {
Text("Upps")
}
}
}
}
Using Undo/Redo with SwiftData in your SwiftUI projects
SwiftData offers undo/redo capabilities. When you create the container, set the ´IsUndoEnabled´ to true:
@main
struct SnippetBoxApp: App {
@Environment(\.undoManager) var undoManager
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Snippet.self, isUndoEnabled: true)
}
}
This will create an UndoManager, which you can access from the context and call the ´undo´ and ´redo´ function like so:
import SwiftUI
struct UndoButton: View {
@Environment(\.modelContext) private var context
var body: some View {
Button(action: {
context.undoManager?.undo()
}, label: {
Text("Undo")
})
}
}
Working with the ModelContext
The container has a main context property, which you can access like so:
@MainActor
var mainContext: ModelContext { get }
You can also create new contexts from the container:
let extraContext = ModelContext(container)
Changes to the ´extraContext´ are not visible to the main context. Only when you call save on the context, will the changes be visible. This behavior is similar to creating a child context in Core Data.
CRUD in SwiftData
To work with your models you use the model context. For example to fetch all folders in the database, fetch the data from the context:
var folders = try context.fetch(FetchDescriptor<Folder>())
You create new objects by inserting them in the model context:
let folder = Folder(name: "new folder")
context.insert(folder)
Similarly, you delete objects by asking the context to delete them:
context.delete(folder)
Updating is even more simple. You can set the new values for the objects directly. Here is an example where I change the title of the folder:
folder.title = "new title"
SwiftData with SwiftUI
SwiftData is made to work well with SwiftUI. For example, you can use the ´@Query´ property wrapper in your SwiftUI views to fetch data. In the following, I am fetching all folder objects sorted by the creation date:
import SwiftUI
import SwiftData
struct FolderListView: View {
@Query(sort: \Folder.creationDate, order: .forward)
var folders: [Folder]
var body: some View {
List {
ForEach(folders) { folder in
NavigationLink(value: folder) {
FolderRow(folder: folder)
}
}
}
}
}
Model classes defined with the @Model
macro conform automatically to Identifiable and Observable.
Conclusion
I’ve walked through the SwiftData stack, taking a closer look at the Schema, Container, and Context. From defining your data model using SwiftData’s Schema macros to efficiently managing data with ModelContainer and ModelContext, you’ve seen how SwiftData simplifies data persistence and management. Moreover, we’ve covered how to leverage SwiftData in SwiftUI applications and how to implement features like undo/redo and iCloud sync.
Remember, the power of SwiftData lies in its ability to streamline and simplify the complex task of data management, so you can focus more on building unique functionalities for your app. With the knowledge you now possess, I’m confident you’ll be able to tackle data management in your SwiftUI projects with more ease and efficiency. Keep practicing and exploring SwiftData. Happy coding!
Further Reading:
Great article. Clean, straight forward and visually relates the new SwiftData approach to a traditional database and schema model. Very well done ( instantly in my saved references ) 🙂