iOS 27 brings four major SwiftData enhancements: sectioned @Query results, a new .codable attribute modifier for third-party types, ResultsObserver for reacting to store changes outside SwiftUI, and HistoryObserver for monitoring persistent history transactions.
β’ Added sectionBy parameter to @Query, returning typed sections directly from the property wrapper
β’ New .codable attribute option allows persisting Codable class types that SwiftData cannot natively introspect
β’ New ResultsObserver class enables SwiftData fetch result observation via Swift Observation outside of SwiftUI views
β’ New HistoryObserver class provides a reactive eventCounter that increments when persistent history transactions are added, filterable by model type and author
β’ ResultsObserver unlocks SwiftData reactivity in non-SwiftUI contexts (game engines, ViewModels, background services) using Swift Observation
β’ The .codable attribute lets you persist opaque third-party class types (like MKMapItem.Identifier) that SwiftData cannot introspect
β’ HistoryObserver simplifies server sync and cross-process change detection without manually polling fetchHistory
Demonstrates ResultsObserver and the new .codable attribute: a MapCameraController outside SwiftUI that reacts to Trip changes using Swift Observation, plus a Trip model storing an MKMapItem.Identifier via the .codable attribute.
import SwiftData
import MapKit
import Observation
// MARK: - Model using new .codable attribute
@Model
final class Trip {
var name: String
var destination: String
var startDate: Date
var endDate: Date
// MKMapItem.Identifier is a class SwiftData can't introspect β
// mark it .codable so SwiftData serializes it via Codable.
@Attribute(.codable)
var mapItemIdentifier: MKMapItem.Identifier?
var latitude: Double
var longitude: Double
init(name: String, destination: String,
startDate: Date, endDate: Date,
latitude: Double = 0, longitude: Double = 0,
mapItemIdentifier: MKMapItem.Identifier? = nil) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
self.latitude = latitude
self.longitude = longitude
self.mapItemIdentifier = mapItemIdentifier
}
}
// MARK: - ResultsObserver outside SwiftUI
@Observable
final class MapCameraController {
var cameraBounds: MapCameraBounds = MapCameraBounds(minimumDistance: 1_000)
private var observer: ResultsObserver<Trip>?
private var observationToken: ObservationTracking.Token?
init(modelContainer: ModelContainer) {
// Create a ResultsObserver β no predicate means all trips.
observer = ResultsObserver<Trip>(
modelContainer: modelContainer
)
// withContinuousObservation with .didSet fires after every change.
observationToken = withContinuousObservation(options: .didSet) { [weak self] in
guard let self else { return }
// Access results to register the observation dependency.
let trips = self.observer?.results ?? []
self.recalculateBounds(for: trips)
}
}
private func recalculateBounds(for trips: [Trip]) {
guard !trips.isEmpty else { return }
let coords = trips.map {
CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude)
}
// Build a region that fits all trip coordinates.
let lats = coords.map(\.latitude)
let lons = coords.map(\.longitude)
let center = CLLocationCoordinate2D(
latitude: (lats.min()! + lats.max()!) / 2,
longitude: (lons.min()! + lons.max()!) / 2
)
let span = MKCoordinateSpan(
latitudeDelta: (lats.max()! - lats.min()!) + 2,
longitudeDelta: (lons.max()! - lons.min()!) + 2
)
let region = MKCoordinateRegion(center: center, span: span)
cameraBounds = MapCameraBounds(
centerCoordinateBounds: region,
minimumDistance: 50_000
)
}
}
// MARK: - Sectioned @Query in SwiftUI
import SwiftUI
struct TripListView: View {
// New sectionBy parameter groups trips by destination string.
@Query(sort: \.startDate, sectionBy: \.destination)
private var trips: [Trip]
var body: some View {
List {
ForEach(_trips.sections) { section in
Section(header: Text(section.id)) {
ForEach(section) { trip in
Text(trip.name)
}
}
}
}
}
}Codable attributes are opaque to SwiftData: they cannot be used in Predicates or SortDescriptors, and schema changes to the underlying type will not trigger automatic migrations. ResultsObserver and HistoryObserver require storing the ObservationTracking token for the observation lifetime β letting it deallocate stops updates. Sectioned @Query sectionBy KeyPath must resolve to a String.
None β available on all devices supporting iOS 27
More iOS 27 APIs land every week.
Get notified when new capabilities are published β no noise, just signal.