SwiftData: Build an App With Persistence
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.

Mazen Kourouche
Jun 10, 2023


Fullsuite is a leading digital solutions agency. Partner with industry experts to develop innovative applications that drive business growth. Fullsuite delivers bespoke digital solutions tailored to your unique needs.
Learn MoreDefining 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:
1@Model2class User {3 var username: String4 var age: Int5}
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:
1@Model2class User {3 var username: String4 var age: Int5 var favoriteBooks: [String]6}
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:
1@Model2class User {3 @Attribute(.unique) var username: String45 // ...6}
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:
1@Model2class Author {3 @Attribute(.unique) var name: String4 var posts: [Post]5}67@Model8class Post {9 var title: String10 var content: String11 var author: Author12}
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
- .cascade: Deletes any related models.
- .deny: Prevents the deletion of a model if it contains references to other models.
- .noAction: Makes no changes to any related models.
- .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.
1@Model2class Post {3 var title: String4 var content: String5 @Relationship(.noAction) var author: Author6}
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.
1let container = ModelContainer([Author.self, Post.self])
SwiftUI's view and scene modifiers also make it easier to establish the container in the views environment.
1import SwiftData23@main4struct SwiftDataBlogPostsApp: App {5 var body: some Scene {6 WindowGroup {7 ContentView()8 .modelContainer(for: [Post.self, User.self])9 }10 }11}
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:
1@Environment(\.modelContext) private var modelContext
Outside the view hierarchy, you can access a shared Main Actor context:
1let context = container.mainContext
Or instantiate a new context for a model container:
1let 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:
1let swiftUIPostsPredicate = #Predicate<Post> {2 $0.title.contains("SwiftUI")3}45let descriptor = FetchDescriptor<Post>(predicate: recentPostsPredicate)6let 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:
1@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:
1import 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:
1@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
- Add
@Modelto the models you want to persist in storage, as well as any referenced models. - Add
@Attribute(.unique)to mark a property as unique. - Add
@Relationship(.cascade)or any other delete rule to control how deletion of referenced objects is managed. - 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. - Create a
modelContextenvironment variable in your SwiftUI view or other (e.g. MVVM) class. - Create a
@Queryvariable to fetch your data within your SwiftUI view, including your desired predicates and sort options. - Use the
modelContextvariable to insert, delete or modify any of the stored objects. - If you require persistence immediately, call
try modelContext.save()