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
- .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.
@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
- Add @Model to 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 modelContext environment variable in your SwiftUI view or other (e.g. MVVM) class.
- Create a @Query variable to fetch your data within your SwiftUI view, including your desired predicates and sort options.
- Use the modelContext variable to insert, delete or modify any of the stored objects.
- If you require persistence immediately, call try modelContext.save()