A Guide to SwiftData

The preceding chapters covered database storage using Core Data. While Core Data is a powerful and flexible solution to data storage, it was created long before the introduction of SwiftUI and lacks the simplicity of SwiftUI’s approach to app development. Introduced in iOS 17, SwiftData addresses this shortcoming by providing a declarative approach to persistent data storage that is tightly integrated with SwiftUI.

This chapter introduces SwiftData and provides a broad overview of the key elements required to store and manage persistent data within iOS apps.

Introducing SwiftData

The SwiftData framework integrates seamlessly with SwiftUI code and offers a declarative way to store persistent data within apps. Implemented as a layer on top of Core Data, SwiftData provides access to many of its features without the need to write complex code.

The rest of this chapter will introduce the SwiftData framework classes and outline how to integrate SwiftData into your iOS app projects. In the next chapter, titled A SwiftData Tutorial, we will create a project that demonstrates persistent data storage using SwiftData.

Model Classes

The SwiftData model classes represent the schema for the data to be stored and are declared as Swift classes. Consider the following class representing the data structure of an address book app:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

class Contact {
    var firstname: String
    var lastname: String
    var address: String

    init(firstname: String, lastname: String, address: String) {
        self.firstname = firstname
        self.lastname = lastname
        self.address = address
    }
}Code language: Swift (swift)

To store the contact information using SwiftData, we need to designate this class as a SwiftData model. To make this declaration, all that is required is to import the SwiftData framework and add the @Model macro to the class:

import SwiftData

@Model
class Contact {
    var firstname: String
    var lastname: String
    var address: String

    init(firstname: String, lastname: String, address: String) {
        self.firstname = firstname
        self.lastname = lastname
        self.address = address
    }
}Code language: Swift (swift)

Model Container

The purpose of the Model Container class is to collect the model schema and generate a database in which to store instances of the data model objects. Essentially, the model container provides an interface between the model schema and the underlying database storage.

Model containers may be created directly or by applying the modelContainer(for:) modifier to a Scene or WindowGroup. In both cases, the container must be passed a list of the models to be managed. The following code, for example, creates a model container for our Contact model:

let modelContainer = try? ModelContainer(for: Contact.self)Code language: Swift (swift)

In the following example, the model container is initialized using three models:

let modelContainer = try? ModelContainer(for: Contact.self, Message.self, CallLog.self)Code language: Swift (swift)

The following code, on the other hand, uses the modelContainer(for:) modifier to create a model container for a WindowGroup:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

var body: some Scene {
    WindowGroup {
        ContentView()
    }
    .modelContainer(for: Contact.self)
}Code language: Swift (swift)

Model Configuration

Model configurations can be applied to model containers to configure how the persistent data is stored and accessed. A model container might, for example, be configured to store the data in memory, in a specific file, or to access data in read-only mode. The following code creates a model configuration for in-memory data storage and applies it to a new model container:

let modelConfig = ModelConfiguration(isStoredInMemoryOnly: true) 
let modelContainer = try? ModelContainer(for: Contact.self, 
                                              configurations: modelConfig)
Code language: Swift (swift)

Model Context

When a model container is created, SwiftUI creates a binding to the container’s model context. The model context tracks changes to the underlying data and provides the programming interface through which the app code performs operations on the stored data, such as adding, updating, fetching, and deleting model objects.

When a model container is created, a binding to the model context is placed into the app’s environment, where it can be accessed from within scenes and views as follows:

@Environment(\.modelContext) var modelContextCode language: Swift (swift)

The model context provides several methods for accessing the database, for example:

// Insert a model object
modelContext.insert(contact)

// Delete a model object
modelContext.delete(contact)

// Save all changes
modelContext.save()Code language: Swift (swift)

Predicates and FetchDescriptors

Predicates define the criteria for fetching matching data from a database and take the form of logical expressions that evaluate to true or false. The following code creates a predicate to filter the contacts whose last name is “Smith”:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

let namePredicate = #Predicate<Contact> { $0.lastname.contains("Smith") }Code language: Swift (swift)

Once the predicate has been declared, it is used to create a FetchDescriptor as follows:

let descriptor = FetchDescriptor<Visitor>(predicate: namePredicate)Code language: Swift (swift)

Finally, the fetch descriptor is passed to the model context’s fetch() method to obtain a list of matching objects:

let theSmiths = try? modelContext.fetch(descriptor)Code language: Swift (swift)

In addition to filtering fetch results, the fetch descriptor can also be used to sort the returned matches. The following descriptor, for example, sorts the fetch results by contact last name:

let descriptor = FetchDescriptor<Visitor>(predicate: namePredicate, 
                            sortBy: [SortDescriptor(\Contact.lastname)])Code language: Swift (swift)

The SortDescriptor may also be used to specify the sorting order of the fetch results. The following SortDescriptor example will reverse the sorting order when used in a fetch() call:

let descriptor = FetchDescriptor<Visitor>(predicate: namePredicate, 
            sortBy: [SortDescriptor(\Contact.lastname, order: .reverse)])
Code language: Swift (swift)

The @Query Macro

The @Query macro provides a convenient way to fetch objects from storage and uses the observability features of SwiftUI to ensure that the results are always up to date. In the simplest form, the @Query macro can be used to fetch all of the stored contact objects from the database:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

@Query var contacts: [Contact]Code language: Swift (swift)

Once declared, the contacts array will automatically update to contain the latest contacts without the need to call the fetch() method on the model context.

The @Query macro can also be used to filter results using predicates and sort descriptors, for example:

@Query(filter: #Predicate<Contact> { $0.lastname.contains("Smith") }, sort: [SortDescriptor(\Contact.lastname, order: .reverse)]) var theSmiths: [Visitor]Code language: Swift (swift)

Model Relationships

Relationships between SwiftData models are declared using the @Relationship macro. Suppose, for example, that our address book app keeps a phone call log for each of our contacts. This will require a model class containing the date and time of the call:

@Model
class CallDate {
    
    var date: Date

    init(date: Date) {
        self.date = date
    }
}Code language: Swift (swift)

To associate a contact with the list of calls, we need to establish a relationship between the Contact and CallDate models. This is achieved using the @Relationship macro in the Contact model as follows:

@Model
class Contact {
    var firstname: String
    var lastname: String
    var address: String

    @Relationship var calls = [CallDate]()

    init(firstname: String, lastname: String, address: String) {
        self.firstname = firstname
        self.lastname = lastname
        self.address = address
    }
}Code language: Swift (swift)

With the relationship established, we can access the calls array property of contact model objects to access the list of associated calls. In the following code, for example, a new call entry is added to a contact’s call log:

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 

contact.calls.append(CallDate(date: Date.now))Code language: Swift (swift)

When a model object is deleted, the default behavior is for any related objects to remain in the database. This means that if we deleted a contact, all of their calls would remain in the database. While this may be the desired behavior in other situations, it does not make sense to keep the log entries in our address book example. To delete all of the call data when a contact is removed, we can specify a deletion rule. In this case, the cascade option is used to remove all related data down through the entire chain of relationships:

@Relationship(deleteRule: .cascade) var calls = [CallDate]()Code language: Swift (swift)

The full list of deletion rules is as follows:

  • cascade – Removes all related objects.
  • deny – Prevents the removal of objects containing relationships with other objects.
  • noAction – Leaves the related objects unchanged, leaving in place references to the deleted objects. • nullify – Does not remove the related objects but nullifies references to the deleted objects.

Model Attributes

The @Attributes macro applies behavior to individual properties in a model class. A common use is to specify unique properties. For example, to prevent duplicate last names, the @Attribute macro would be used as follows:

@Model
class Contact {
    var firstname: String
    @Attribute(.unique) var lastname: String
    var address: String

    @Relationship var calls = [CallDate]()

    init(firstname: String, lastname: String, address: String) {
        self.firstname = firstname
        self.lastname = lastname
        self.address = address
    }
}Code language: Swift (swift)

Finally, a property within a model class may be excluded from being stored in the database using the @Transient macro:

@Model
class Contact {
    var firstname: String
    var lastname: String
    var address: String
    @Transient var tempAddr: StringCode language: Swift (swift)

Summary

SwiftData combines many of the features of Core Data with the convenience of SwiftUI to provide a simple way to store persistent data in iOS apps. The database schema are declared as Swift classes and adapted into SwiftData models using the @Model macro. The model container collects the model classes and uses them to create and manage the underlying database system. The model context tracks changes to the data and provides a programming interface for adding, searching, and modifying the stored data objects. Data is fetched using predicates, fetch descriptors, and the @Query macro. Relationships between models are established using the @Relationship macro, while the @Attributes macro allows rules to be applied to individual model class properties.

 

 

You are reading a sample chapter from iOS 17 App Development Essentials.

Buy the full book now in eBook (PDF and ePub) or Print format.

The full book contains 68 chapters, over 580 pages of in-depth information, and downloadable source code.

Learn more.

Preview  Buy eBook  Buy Print

 


Categories