SwiftData: Build an App With Persistence

Author Image

Mazen Kourouche

Jun 9, 2023

SwiftData Cover Photo

This year (WWDC23) Apple announced SwiftData, a truly game-changing framework that leverages Swift's macro system, seamlessly integrates with SwiftUI, and works hand in hand with other frameworks like CloudKit and Widgets. SwiftData makes it easier to persist data, model schema, and manage your app’s persistence layer - similar to CoreData, but with Swift.

Defining the Model with @Model

In SwiftData, we use the @Model macro to define our data model schema. By decorating your model with this macro, SwiftData auto-magically transforms your stored properties into persisted ones.

Consider a simple user model:

@Model class User { var username: String var age: Int }

In this example, the User model with a username and an age property is defined. SwiftData will automatically create a schema for this model and handle the persistence for you. You can take this project as a sample to build your app with SwiftData.

Basic and Complex Value Types

SwiftData supports a wide range of value types, from simple ones such as Strings, Ints, and Floats, to more complex types like Enums, Structs, and Codables. Collections of these value types are also supported.

For instance, you might have a User model that has an array of favourite book titles:

@Model class User { var username: String var age: Int var favoriteBooks: [String] }

Schema Macros for Control

SwiftData offers schema macros that enable developers to control how properties are inferred. Here's a quick overview of the primary schema macros you can use:

  • @Attribute: Allows you to add attributes to your model schema.
  • .unique: Models uniqueness constraints, enabling upsert operations.
  • originalName: String: Allows you to rename a variable without losing data.
  • @Relationship: Controls the choice of inverses and delete propagation rules.
  • @Transient: Instructs SwiftData to ignore certain stored properties.

These macros provide granular control over how your models are handled, making it easier to enforce constraints and manage relationships.

Let’s take our User model for example. We can specify that the username property must be unique, so SwiftData knows that is we attempt to add another value to the persisted storage, with the same username value, it will instead update the existing one instead of inserting a new one.

We would do this as follows:

@Model class User { @Attribute(.unique) var username: String // ... }

Establishing Relationships

In SwiftData, we model relationships between types (classes) through references, enabling you to create links between your model types. Let's consider a blogging app, where an Author can have multiple Posts:

@Model class Author { @Attribute(.unique) var name: String var posts: [Post] } @Model class Post { var title: String var content: String var author: Author }

In this scenario, if the Post model references the Author model, then the Author model must also be decorated with the @Model macro.

Relationship Delete Rule

When deleting a model that references other models, you need to describe the rule to apply. Here are the options:

  • [.cascade](<https://developer.apple.com/documentation/swiftdata/relationshipdeleterule/cascade>): Deletes any related models.
  • [.deny](<https://developer.apple.com/documentation/swiftdata/relationshipdeleterule/deny>): Prevents the deletion of a model if it contains references to other models.
  • [.noAction](<https://developer.apple.com/documentation/swiftdata/relationshipdeleterule/noaction>): Makes no changes to any related models.
  • [.nullify](<https://developer.apple.com/documentation/swiftdata/relationshipdeleterule/nullify>): Nullifies the related model’s reference to the deleted model.

To note that we want an author to be deleted if we delete a post that references it, we could add the .cascade delete rule, but that its probably not what we’d want in this scenario, so we’ll use .noAction.

@Model class Post { var title: String var content: String @Relationship(.noAction) var author: Author }

Containers and Context: The Building Blocks of Persistence

Two crucial concepts in SwiftData are ModelContainer and ModelContext, which together create the persistent backend for your model types.

Model Container

The ModelContainer is responsible for creating the persistent backend for your model types. To create a ModelContainer, specify the list of model types you want to store. You can also configure it further by specifying the URL where you want the data stored.

let container = ModelContainer([Author.self, Post.self])

SwiftUI's view and scene modifiers also make it easier to establish the container in the views environment.

import SwiftData @main struct SwiftDataBlogPostsApp: App { var body: some Scene { WindowGroup { ContentView() .modelContainer(for: [Post.self, User.self]) } } }

You need at least one model container to use SwiftData, but you can use as many containers as you need for different view hierarchies.

Model Context

ModelContext is your interface for interacting with your model. It observes all changes to your models and provides actions you can perform, like saving, deleting, or undoing changes. In SwiftUI, you'll typically get your ModelContext from your view's environment:

@Environment(\.modelContext) private var modelContext

Outside the view hierarchy, you can access a shared Main Actor context:

let context = container.mainContext

Or instantiate a new context for a model container:

let context = ModelContext(container)

Fetching Data

SwiftData offers various ways to fetch and modify data, all driven by the ModelContext. With Swift's native SortDescriptor and FetchDescriptor, you can sort and filter the data you fetch.

An example predicate looks like this:

let swiftUIPostsPredicate = #Predicate<Post> { $0.title.contains("SwiftUI") } let descriptor = FetchDescriptor<Post>(predicate: recentPostsPredicate) let recentPosts = try context.fetch(descriptor)

In this case, we're fetching all the posts with “SwiftUI” in their title.

The true beauty of SwiftData lies in its seamless integration with SwiftUI. The @Query property wrapper lets you load and filter anything in the database right within SwiftUI:

@Query(filter: swiftUIPostsPredicate, sort: \.title, order: .reverse) var swiftUIPosts: [Post]

SwiftData supports the new @Observable feature, creating automatic dependencies between a view and its data and binding model data to the UI.

Modifying Data

Modifying data is as simple as calling context.insert(object), context.delete(object), or using the standard property setters. What’s great is that you also don’t need to explicitly call try context.save() after making modifications to persist changes - SwiftData autosaves changes based on certain UI changes or user actions. Keep in mind though that in cases where you know you want data to persist immediately, you can use try context.save() to ensure the data has been committed to storage.

Adding SwiftData to an App

To start using SwiftData, import the framework:

import SwiftData

Next, create a container for your app and add the @Model decorator to your model type. You can then use @Query in SwiftUI to retrieve your data. Accessing the context is as easy as using the @Environment property wrapper:

@Environment(\.modelContext) private var modelContext

With that, you're all set to start leveraging SwiftData's power in your application!

In essence, SwiftData streamlines persistence in SwiftUI apps, allowing developers to focus more on building outstanding user experiences. It's another testament to how Apple continues to evolve its frameworks in a developer-friendly manner. Happy coding!

TL:DR Round-up

  1. Add @Model to the models you want to persist in storage, as well as any referenced models.
  2. Add @Attribute(.unique) to mark a property as unique.
  3. Add @Relationship(.cascade) or any other delete rule to control how deletion of referenced objects is managed.
  4. Initialise a container using let container = ModelContainer([Author.self, Post.self]) or adding the .modelContainer(for: Post.self) to your view (generally in your App file), specifying the model types you want to persist.
  5. Create a modelContext environment variable in your SwiftUI view or other (e.g. MVVM) class.
  6. Create a @Query variable to fetch your data within your SwiftUI view, including your desired predicates and sort options.
  7. Use the modelContext variable to insert, delete or modify any of the stored objects.
  8. If you require persistence immediately, call try modelContext.save()