SwiftData in Swift UI

Manish Pathak
5 min readApr 28, 2024

--

What is SwiftData ?

SwiftData is a powerful framework for data modeling and management. It uses Swift’s new macro system to create a seamless API experience. It is naturally integrated with SwiftUI and works with other platform features, like CloudKit and Widgets.

It provides a declarative way to define your data models, relationships, and migrations

Features of SwiftData

  • Expressive data model definition: Define your models in code using Swift’s powerful type system.
  • Automatic data migration: SwiftData can automatically migrate your data between different versions of your application.
  • Concurrency support: SwiftData takes advantage of Swift’s concurrency features, allowing you to perform data operations asynchronously.
  • Cloud synchronization: SwiftData integrates with iCloud, allowing you to keep your data synchronized across multiple devices.
  • Security: SwiftData uses the latest security technologies to protect your data.

Comparison with Core Data

The SwiftData is based on Core Data, it offers several important improvements.

Difference between Cor Data and SwiftData
  • SwiftData leverages Swift compiler for checking. Core Data errors are runtime-only.
  • SwiftData relationships use regular reference types like arrays. Core Data has dedicated relationship attributes and configs.
  • SwiftData observes changes automatically. Core Data requires explicitly configuring KVO observers.
  • SwiftData has native property wrappers like @Model and @Query. Core Data doesn’t have first-class SwiftUI support.

How to use Swift Data ?

Using the model macro:

@Model is a Swift macro that helps to define the model’s schema from the Swift code. SwiftData schemas are normal Swift code, but when needed, can be used to annotate properties with additional metadata. Using this schema, SwiftData adds powerful functionality to the model objects.

// Adding @Model to Trip

import SwiftData

@Model
class Trip {
var name: String
var destination: String
var endDate: Date
var startDate: Date

var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}

Models in SwiftData are the source of truth and will transform the class’ stored properties into persisted properties.

Using the attribute:

SwiftData natively adapts value-type properties like string, int, and float. and complex value type like such as structs, enums, and codable.

Using the Relationship:

Relationships are inferred from reference types

- Other model types
- Collections of model types

SwiftData models reference types as relationships.

Additional Metadata:

@Model will modify all the stored properties on a custom type using metadata like:

  • @Attribute(.unique) for uniqueness constraint
  • @Relationship for choice of inverses and delete propagation rules
  • @Transient to exclude property from model
  • @Relationship can be used to control the choice of inverses and specify delete propagation rules. These change the behaviors of links between models. Use the Transient macro to tell SwiftData not to include specific properties. Here is an example.
// Providing additional metadata

import SwiftData

@Model
class Trip {
@Attribute(.unique) var name: String
var destination: String
var endDate: Date
var startDate: Date

@Relationship(.cascade) var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}

Working with your data

Model container

The Model container provides the persistent backend

- Customized with configurations. Use the default settings just by specifying the schema.
- Provides schema migration options
- Create a container by specifying the list of model types to store — optionally provide ModelConfiguration with an url, CloudKit and group container identifiers, and migration options

let container = try ModelContainer(for: [Trip.self, LivingAccommodation.self], configurations: ModelConfiguration(url: URL("path")))
  • Or in SwiftUI with the modifier
.modelContainer(for: [Trip.self, LivingAccommodation.self])`

ModelContext

With the container set up, fetch and save data with model contexts. Also can be used with a SwiftUI’s view and scene modifiers to set up a container and have it in the view’s environment.

import SwiftData 
import SwiftUI

@main
struct TripsApp: App {

var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for:
[Trip.self,
LivingAccommodation.self])
)
}
}

Model contexts observe all the changes to the models and provide many of the actions to operate on them.

  • Tracking updates
  • Fetching models
  • Saving changes
  • Undoing changes

In SwiftUI, generally the modelContext is from the view’s environment.

import SwiftData 
import SwiftUI

struct ContextView : View {
@Environment(\.modelContext) private var context
}
// or outside the view hierarchy, a shared main actor bound context,
let context = container.mainContext
// or instantiate new contexts for a given model container.
let context = ModelContext(container)

Fetching Data

New in iOS 17, predicate works with native Swift types and uses Swift macros for strongly typed construction. It’s a modern replacement for NSPredicate.

New Swift native types:
- Predicate
- FetchDescriptor
Improvements to SortDescriptor

Predicate

  • Fully type checked.
  • #Predicate construction instead of text parsing
  • Autocompleted keypaths.

Here are a few examples of building predicates.

// I can specify all the trips whose destination is New York.
let tripPredicate = #Predicate<Trip> {
$0.destination == "New York"
}

// I can narrow our query down to just trips about birthdays
let tripPredicate = #Predicate<Trip> {
$0.destination == "New York" && $0.name.contains("birthday")
}
// and I can specify we're only interested in trips planned for the future, as opposed to any of our past adventures.
let today = Date()
let tripPredicate = #Predicate<Trip> {
$0.destination == "New York" &&
$0.name.contains("birthday") &&
$0.startDate > today
}

We can use the new FetchDescriptor type and instruct our ModelContext to fetch those trips.

let descriptor = FetchDescriptor<Trip>(predicate: tripPredicate)
let trips = try context.fetch (descriptor)

SortDescriptor

  • Updated to support all Comparable types
  • Swift native keypaths

Swift SortDescriptor works together with FetchDescriptor and is getting some updates to support native Swift types and keypaths

let descriptor = FetchDescriptor<Trip>(
sortBy: SortDescriptor(\Trip.name),
predicate: tripPredicate
)
let trips = try context.fetch(descriptor)

More FetchDescriptor options

  • relationships to prefetch
  • result limits
  • exclude unsaved changes and more

Modifying Data

Basic operations

  • Inserting
  • Deleting
  • Saving
  • Changing

Delete persistent objects marking them for deletion, and committing them to the persistent container.

var myTrip = Trip(name: "Birthday Trip", destination: "New York")

// Insert a new trip
context.insert(myTrip)
// Delete an existing trip
context.delete(myTrip)
// Manually save changes to the context
try context.save()
  • The @Model macro modifies stored properties for change tracking and observation
  • Updated automatically by the ModelContext

Use SwiftData in SwiftUI

  • Seamless integration with SwiftUI
  • Easy configuration
  • Automatically fetch data and update views

View modifiers

  • Configure data store with .modelContainer which is propagated throughout SwiftUI environment
  • Fetching in SwiftUI with @Query
  • No need for @Published and SwiftUI automatically updates
// @Query

import SwiftData
import SwiftUI

struct ContentView: View {
@Query(sort: \.startDate, order: .reverse) var trips: [Trip]
@Environment(\.modelContext) var modelContext

var body: some View {
NavigationStack() {
List {
ForEach(trips) { trip in
// ...
}
}
}
}
}

Observing changes

  • No need for @Published
  • SwiftUl automatically refreshes

SwiftData supports the all-new observable feature for your modeled properties. SwiftUI will automatically refresh changes on any of the observed properties. SwiftUI and SwiftData work hand in hand

--

--