What's new in SwiftData
Learn SwiftData updates for sectioned @Query fetches, Codable attributes, and store/history observation outside SwiftUI.
TL;DR
- @Query gains a sectionBy: parameter for grouping SwiftData fetch results in SwiftUI while keeping the wrapped value as the fetched model array.
- @Attribute(.codable) lets SwiftData persist Codable types it can't schema-inspect, such as MapKit's MKMapItem.Identifier, with important filtering, sorting, and migration limits.
- ResultsObserver brings Query-like fetching and change observation to non-SwiftUI code using Swift Observation.
- HistoryObserver exposes persistent-history changes through an observable eventCounter for sync, extensions, and other transaction-driven workflows.
Section SwiftData fetches in SwiftUI
SwiftData's Query supports sectioned fetching by passing a key path to the new sectionBy: parameter. The key path starts at the model type and resolves to a String used as the section identity.
The @Query wrapped value remains an array of models, so existing code can continue to consume the fetched results. To render sections, access the property wrapper with its underscore-prefixed name and iterate its sections collection.
- Use @Query(sort:sectionBy:) to fetch sorted results grouped by a model key path.
- Each section exposes an id, which is the value produced by the sectionBy key path.
- Each section is itself a collection of models, suitable for an inner ForEach in SwiftUI.
- The example groups Trip models by destination while sorting by startDate.
Sectioned fetching with @Query
Access _trips.sections to render grouped results while trips remains the normal array of fetched Trip models.
struct TripListView: View {
@Query(sort: \Trip.startDate, sectionBy: \.destination)
var trips: [Trip]
var body: some View {
List(selection: $selection) {
ForEach(_trips.sections) { section in
Section(section.id) {
ForEach(section) { trip in
TripListItem(trip: trip)
}
}
}
}
}
}Persist custom and third-party types with Codable
SwiftData normally builds a schema by inspecting model properties. Types from other frameworks can fail schema generation when SwiftData can't inspect them, such as a non-@Model class type.
Marking an attribute with @Attribute(.codable) tells SwiftData to persist the type's encoded representation instead of deriving a schema for it. This is intended as an escape hatch for types you don't own.
- Useful for external Codable types such as MKMapItem.Identifier.
- Codable attributes are opaque to SwiftData.
- They can't be used in predicates for filtering or in sort descriptors for sorting.
- Shape changes inside the Codable type don't trigger SwiftData migration; the type's Codable implementation must remain forward- and backward-compatible.
- For types you define, prefer @Model or supported value types to preserve filtering, sorting, indexing, and migration behavior.
Storing a Codable MapKit identifier
Use @Attribute(.codable) for the MapKit identifier while ordinary SwiftData-supported values remain modeled normally.
import SwiftData
@Model
class Trip {
struct Location: Codable {
var latitude: Double
var longitude: Double
}
var name: String
var destination: String
var startDate: Date
var endDate: Date
var location: Location?
@Attribute(.codable)
var mapItemIdentifier: MKMapItem.Identifier?
}Observe query results outside SwiftUI
@Query remains the first choice for SwiftUI views because it fetches data and automatically refreshes the view when relevant store changes occur. For non-SwiftUI code, ResultsObserver provides similar fetch-and-observe behavior using Swift Observation.
ResultsObserver supports familiar query primitives including filtering, sorting, and sectioning. The session demonstrates using it from a MapCameraController that recalculates MapCameraBounds whenever the Trip results change.
- Create a ResultsObserver for the model type and model context.
- Use withContinuousObservation(options: [.didSet]) to run code after observed results change.
- Store the ObservationTracking.Token to keep the observation alive for the lifetime of the owning object.
- Use this pattern for state objects, delegate-based architectures, games, controllers, or other non-view code that depends on SwiftData results.
Recompute map bounds when trips change
A non-view controller observes Trip results and updates derived map state whenever the store changes.
@Observable
@MainActor
final class MapCameraController {
private let modelResultsObserver: ModelResultsObserver<Trip>
var bounds: MapCameraBounds?
private var token: ObservationTracking.Token?
init(modelContext: ModelContext) throws {
modelResultsObserver = try ModelResultsObserver<Trip>(modelContext: modelContext)
token = withContinuousObservation(options: [.didSet]) { [weak self] event in
self?.bounds = self?.calculateBounds(trips: modelResultsObserver.results)
}
}
private func calculateBounds(trips: [Trip]) -> MapCameraBounds? {
/* ... */
}
}Observe persistent history for sync and external changes
SwiftData records persistent-history transactions when the store is saved. Transactions describe what changed, where the change came from, and include tokens that can be used with ModelContext.fetchHistory() to fetch newer transactions.
HistoryObserver watches persistent history and increments a single observable eventCounter when new transactions are available. Code observes that counter, then fetches and processes the relevant history.
- Use HistoryObserver when you need transaction-level awareness rather than just current query results.
- Filter observation by model type or transaction author when only some changes matter.
- A server sync workflow can observe only app-authored transactions to avoid replaying server-originated changes back to the server.
- Call ModelContext.fetchHistory() from the processing step to fetch and handle transactions.
Trigger server sync from history changes
Observing eventCounter tells Swift Observation what to track; processChanges can then fetch history and upload changes.
@SyncActor
final class ServerSync {
private let observer: HistoryObserver
private var token: ObservationTracking.Token?
func start() throws {
self.observer = try HistoryObserver(authors: ["App"], modelContainer: modelContainer)
token = withContinuousObservation(options: .didSet) { [weak self] _ in
_ = self?.observer.eventCounter
self?.processChanges()
}
}
private func processChanges() {
// Fetch and process history transactions.
}
}Adoption guidance
Use the highest-level API that matches the part of the app you're building: @Query in SwiftUI views, ResultsObserver for live fetched results elsewhere, and HistoryObserver for persistent-history-driven workflows.
For persistence modeling, keep Codable attributes narrow. They are best suited to external framework types that SwiftData cannot inspect, not as a replacement for well-modeled app-owned data.
- Start with @Query for SwiftUI list/detail UI.
- Use sectionBy: when grouped presentation is needed directly from a fetch.
- Use @Attribute(.codable) only when native SwiftData modeling is not practical.
- Use ResultsObserver for derived state from current store contents.
- Use HistoryObserver plus ModelContext.fetchHistory() for synchronization, app extensions, and transaction processing.
Resources
Related Sessions
Create live communication experiences
Use LiveCommunicationKit to present VoIP conversations in system UI, handle incoming and outgoing calls, and manage group membership and merging.
Code-along: Add persistence with SwiftData
Add SwiftData persistence to an existing SwiftUI app by converting state to @Model schemas, relationships, model containers, and targeted @Query fetches.