SwiftData in Swift UI
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.
- 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