SwiftData — Get rid of the CoreData complexity

Santosh Botre
6 min readJun 7, 2023

Declarative code integration with persistent data has been made easy with the help of SwiftData.

We can query and filter data using regular Swift code. It’s designed to integrate seamlessly with SwiftUI.

Step 1: Create Models with @Model

What is @Model?

@Model is a macro which Converts a Swift class into a stored model that’s managed by SwiftData.

Let’s try to upgrade one of our simple Data Model to a Persistent Model.

In our projects, the Data Models are either class or struct. Which are regular Swift types.

We can make them persistent Models by just adding @Model at the top with no additional files or tools to manage.

Example:

We have a struct called Person with some properties in it.

Hmm…. It says the Non-Class type cannot conform to the class protocol PersistentModel.

Which means it can be applied to class types only.

Go back to the macro definition of the @Model -> It says it is a macro which Converts a Swift class into a stored model that’s managed by SwiftData.

What is class Protocol PersistentModel?

It is an interface that enables SwiftData to manage a Swift class as a stored model.

Let’s change our struct type Data Model to class type first.

And it compiled….. Because SwiftData includes all non-computed properties of a class as long as they use compatible types.

NOTE: The SwiftData supports primitive types such as Bool, Int, and String, as well as complex value types such as structures, enumerations, and other value types that conform to the Codable protocol.

The @Model macro updates the class with conformance to the PersistentModel protocol, which SwiftData uses to examine the class and generate an internal schema. Additionally, the macro enables change tracking for the class by adding conformance to the Observable protocol.

It’s sufficient to meet the basic needs of the persistence model.

@Model is Observable by design, so there is no need for @Published and SwiftUI automatically refreshes the view on change.

Step 2: Configure the model storage

The modelContainer simplifies the creation and management of the Swift Data stack by handling the creation of the managed object model.

SwiftData will examine our models and generate the required schema.

The question is, which models to persist? and which underlying storage it should use, like in-memory, on a device, or in iCloud using the CloudKit.

We have to set the modelContainer in the view for storing the provided model type, creating a new container if needed.

In the above code snippet, we specified the modelContainer and provided our model type Person.

By default, modelContainer persists models on the device and autosave is enabled.

We can pass the arguments as per our requirements. Also, can implement a callback that will be invoked when the creation of the container has succeeded or failed.

func modelContainer( for modelType: any PersistentModel.Type, 
inMemory: Bool = false,
isAutosaveEnabled: Bool = true,
isUndoEnabled: Bool = false,
onSetup: @escaping (Result<ModelContainer, any Error>) -> Void = { _ in } ) -> some View

modelContainer also sets a model context for the container in this view’s environment.

In case, you have more models use the overridden method with the argument-type array. i.e., modelTypes

func modelContainer(
for modelTypes: [any PersistentModel.Type],
inMemory: Bool = false,
isAutosaveEnabled: Bool = true,
isUndoEnabled: Bool = false,
onSetup: @escaping (Result<ModelContainer, any Error>) -> Void = { _ in }
) -> some View

Step 2: Access the modelContext

What is modelContext?

The modelContext is the object responsible for the in-memory model data and coordination with the model container to successfully persist that data.

To get a context for our model container bound to the main actor, we have to use the modelContextenvironment variable.

@Environment(\.modelContext) private var modelContext

How to work a model with the help of modelContext?

- Insert

   let person = Person(firstName: "Santosh", lastName: "Botre", age:10)
modelContext.insert(person)

AsautosaveEnabled property to true, Context periodically checks whether it contains unsaved changes, and if so, implicitly saves those changes on our behalf.

We can save immediately by invoking the context’s save() method

- Query

We have the data model persists we’ll likely want to retrieve t model instances, and display them in the view.

@Query is a property wrapper that fetches a set of models and keeps those models in sync with the underlying data.

  @Query private var people: [Person]

We have all the people from the persistent fetched to use.

We can apply search criteria and preferred sort order, using @Query.

@Query(sort: \.firstName, order: .reverse) var people: [Person]

We can apply filter criteria using Predicate macro and preferred sort order, using @Query.

  @Query(filter: #Predicate<Note> { person in
person.age > 18
}, sort: \.firstName, order: .reverse) var people: [Person]

- Delete

We can delete the specific record.

modelContext.delete(person)
We are ready to play with the use SwiftData now :)

High-Level Concerns and Issues-

I referred fatbobman ( 东坡肘子) article WWDC 23 First Impressions just reordered and written with my perspective.

  • This is the 1st release SwiftData. Mostly iOS application deployment strategy is to support the current-2 means (iOS17, iOS 16, iOS 15). As per Apple's history, we all know we can’t use to keep our distribution strategy i.e., current — 2.
  • Even want to use support only iOS 17+ applications due to several existing issues as per impression from beta not recommended due to issues like,

- .transformable attribute is not working.

- Query (an alternative to FetchRequest) does not provide dynamic switching predicates.

  • ModelContext is not completely thread-safe.
  • Data created through other contexts (ModelContext) is not automatically merged into the view context in the current version.
  • Cloud sync for public and shared data is not currently supported.

Do read fatbobman ( 东坡肘子) article WWDC 23 First Impressions.

Pro Tips……

How to Debug SwiftData?

Edit Scheme -> Select Run ->Arguments and add the following :

-com.apple.CoreData.SQLDebug 1

It still works :)

Please don't complaint if you feel this….

How can we specific behaviour of these properties? For example, unique across all models of the same type.

We can add @Attribute to the specific properties.

 @Attribute(.unique) var firstName: String

.unique — Ensures the property’s value is unique across all models of the same type.

.transient — Enables the context to disregard the property when saving the owning model.

.transformable — Transforms the property’s value between an in-memory form and a persisted form.

.externalStorage — Stores the property’s value as binary data adjacent to the model storage.

.encrypt — Stores the property’s value in an encrypted form.

How to Set and Manage Relationships?

We can add @Relationship to the specific properties. Specifies the options that SwiftData needs to manage the annotated property as a relationship between two models.

 @Relationship(.cascade) var owner: Owner
Relationship(
_ deleteRule: RelationshipDeleteRule = .nullify,
_ options: PropertyOptions...,
renamingIdentifier: String? = nil,
inverse: AnyKeyPath? = nil,
hashModifier: String? = nil
) -> ()

--

--

Santosh Botre

Take your time to learn before develop, examine to make it better, and eventually blog your learnings.