SwiftData Stack: Understanding Schema, Container & Context

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.
SwiftData stack diagram including the schema, container an context
The SwiftData stack consists of the Schema, container, and view context.

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.

Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

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:

adding iCloud sync in Xcode project capabilities
Go to project, your target and select new capabilities.
Xcode capabilities add iCloud sync
Xcode with iCloud sync to setup new container
Create a new container for iCloud sync. This is the identifier for your iCloud storage.

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:

1 thought on “SwiftData Stack: Understanding Schema, Container & Context”

  1. 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 ) 🙂

    Reply

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form