SwiftData: Persisting Data Using Declarative Code

At this year's Worldwide Developers Conference (WWDC), Apple introduced many new technologies to help reduce the work required to create powerful applications. Among these advancements is Macros, which helps expand our productivity by negating the need for writing repetitive code in Swift and generating that part of the source code at compilation time. The other new feature is SwiftData, which uses macros to reduce the code to handle data management within Swift applications. 

We'll explore what SwiftData is, how it works, important keywords, and some code examples. To learn more about macros, you can read the documentation available  here.

How SwiftData Works

Let's dive into the core functionality of SwiftData and understand how it operates within Swift applications:


1. The @Model Macro

Models in SwiftData are the source of truth for your application's schema and drive the persistence experience. SwiftData natively adapts your value type properties to be used as attributes and models reference types as relationships.

The @Model allows us to:

Define the Schema: It outlines the object graph of your application, referred to as the Schema. This Schema defines the data structure that you want to persist.

Provides an Interface: The @Model macro also acts as an interface for coding against your data. It enables your Swift classes to interact with the underlying data seamlessly.


import SwiftData

// Annotate new or existing model classes with the @Model macro.


class Trip {

   var name: String

   var destination: String

   var startDate: Date

   var endDate: Date

   var accommodation: Accommodation?


2. Schema and ModelContainer

The Schema, as defined by the @Model macro, is applied to a ModelContainer. This container serves as the bridge between in-memory objects and their storage on a device. It plays a crucial role in managing how data is persisted and retrieved. Let's take a closer look:

let configuration = ModelConfiguration(isStoredInMemoryOnly: true, allowsSave: false)
let container = try ModelContainer(
   for: Trip.self,
   configurations: configuration

The `ModelContainer` can be created quickly, adapting to your application's evolving complexity. If a model type contains a relationship, you may omit the destination model type from the array. SwiftData automatically traverses a model's relationships and includes any destination model types for you.

3. ModelConfiguration

ModelConfiguration complements the ModelContainer by specifying the persistence requirements of your application. Key aspects include:

- Storage Location: You can define whether to store data in memory for transient purposes or on disk for persistent storage.

- File URLs: ModelConfiguration allows you to specify file URLs for data storage. You can provide a custom URL or let SwiftData generate one based on your application's entitlements.

- CloudKit Integration: For applications utilizing CloudKit for syncing, ModelConfiguration facilitates seamless integration, ensuring data is synchronized efficiently.

4. SwiftUI Integration

SwiftData seamlessly integrates with SwiftUI using the modelContainer  modifier. This integration empowers any view to access persisted data easily. If you use the view modifier, add it at the very top of the view hierarchy, so all nested views inherit the properly configured environment: 

import SwiftUI
import SwiftData

struct TripsApp: App {
   var body: some Scene {
       WindowGroup {
               .modelContainer(for: [

Then, from any view down in the hierarchy, you can access the model context by simply doing:

import SwiftUI
import SwiftData

struct ContentView: View {
   @Environment(\.modelContext) private var context

5. ModelContext: The Data Manager

To manage instances of your model classes at runtime, use a model context — the object responsible for the in-memory model data and coordination with the model container to persist that data successfully.

A model context allows:

- Data Fetching: Data objects are fetched into the model context as needed. For example, data objects are fetched into the context when a view loads.

- Tracking Changes: Any changes made to data objects within the model context are tracked and recorded as snapshots. This tracking and recording allow you to use features like undo, redo, and autosave out of the box without additional work.

- Persistence Control: The context manages the persistence of changes. Data changes are persisted when you explicitly call `context.save()`. This behavior means that even though a deleted item might not be visible in a list, it still exists in the model context until the changes are saved. You can also rely on the autosave feature of the context. In this case, iOS will determine when to save your changes (like when sending the app to the background).


6. Fetching Models

SwiftData provides the Query property wrapper and the FetchDescriptor type for performing fetches. To fetch model instances, use @Query in your SwiftUI view. The @Model macro adds Observable conformance to your model classes, enabling SwiftUI to refresh the containing view whenever changes occur to any fetched instances.

import SwiftUI
import SwiftData

struct ContentView: View {
   @Query(sort: \.startDate, order: .reverse) var allTrips: [Trip]
   var body: some View {
       List {
           ForEach(allTrips) {
               TripView(for: $0)

Alternatively, you could use a FetchDescriptor, and use Swift’s safe type syntax to make custom queries and retrieve data for more processing. This can be easily accomplished by doing:


let context = container.mainContext
let upcomingTrips = FetchDescriptor<Trip>(
   predicate: #Predicate { $0.startDate > Date.now },
   sort: \.startDate

upcomingTrips.fetchLimit = 50
upcomingTrips.includePendingChanges = true
let results = context.fetch(upcomingTrips)


SwiftData has revolutionized data management within Swift applications. By simplifying data persistence, providing powerful tools like FetchDescriptor and Query, and seamlessly integrating with SwiftUI, SwiftData empowers developers to create robust and efficient applications.




This piece was written by Marco Salazar, Principal Software Engineer at Encora.  


About Encora

Fast-growing tech companies partner with Encora to outsource product development and drive growth. Contact us to learn more about our software engineering capabilities.





Share this post

Table of Contents