SwiftData - iOS 27, iPadOS 27, macOS 27
What's New in SwiftData
SwiftData's WWDC26 changes for iOS 27, iPadOS 27, macOS 27, watchOS 27, tvOS 27, and visionOS 27 remove several day-to-day rough edges: grouped SwiftUI lists, better persistence for framework types, and observation APIs that work outside views.
Highlights
@Query can group fetched results with sectionBy:, then expose sections through the backing query.
@Attribute(.codable) stores custom or third-party Codable values that SwiftData cannot model directly.
Query-style model observation now works in controllers, actors, and other non-SwiftUI state objects.
HistoryObserverA small observable counter lets sync code wake up when persistent-history transactions arrive.
Sectioned Fetches
This is the most immediately useful change for SwiftUI apps. @Query now accepts a
sectionBy: key path, so a list can be fetched, sorted, and grouped in one place.
The section data lives on the generated backing query, which you access with the underscore-prefixed property.
Fallback for OS 26
Keep the existing unsectioned @Query and derive sections in view code.
If you also ship the OS 27 version, keep the sectioned-query view in a separate
@available type so the property-wrapper initializer is only used where it exists.
@Query(sort: \Trip.startDate) private var trips: [Trip]
var sections: [(name: String, trips: [Trip])] {
Dictionary(grouping: trips, by: \.destination)
.map { (name: $0.key, trips: $0.value) }
.sorted { $0.name < $1.name }
}
struct TripListView: View {
@Query(sort: \Trip.startDate, sectionBy: \.destination)
var trips: [Trip]
var body: some View {
List {
ForEach(_trips.sections) { section in
Section(section.id) {
ForEach(section) { trip in
TripListItem(trip: trip)
}
}
}
}
}
}
Use this when the grouping is part of the data shape: trips by destination, tasks by project, transactions by month. Keep ad-hoc display-only grouping in view code if it is cheap and not reused.
Codable Attributes
@Attribute(.codable) is the new escape hatch for values SwiftData cannot break into model columns.
Apple's example uses MKMapItem.Identifier, a framework type that is Codable but not a native SwiftData value.
Fallback for OS 26
Persist a supported SwiftData type such as Data or String, then expose a computed
property that encodes and decodes the real value. This keeps the model shape stable for older deployments.
var locationData: Data?
var location: Location? {
get {
locationData.flatMap { try? JSONDecoder().decode(Location.self, from: $0) }
}
set {
locationData = try? newValue.map { try JSONEncoder().encode($0) }
}
}
@Model
final class Trip {
struct Location: Codable {
var latitude: Double
var longitude: Double
}
var name: String
var destination: String
var location: Location?
@Attribute(.codable)
var mapItemIdentifier: MKMapItem.Identifier?
}
Treat codable storage as a bridge for external types. For types you own, a normal model or supported value type remains better because SwiftData can filter, sort, migrate, and inspect it more directly.
ResultsObserver
Before this release, SwiftData observation was easiest inside SwiftUI. ResultsObserver brings
a similar result stream to regular app objects, which is useful for map cameras, caches, sidebar state,
widgets, or any controller that should react when a model query changes.
Fallback for OS 26
For UI-driven state, a tiny SwiftUI bridge can keep using @Query and forward snapshots to
a controller. For service objects, use explicit refetches after app-owned writes, imports, or sync events.
struct TripObserverBridge: View {
@Query(sort: \Trip.startDate) private var trips: [Trip]
let update: ([Trip]) -> Void
var body: some View {
Color.clear
.onAppear { update(trips) }
.onChange(of: trips.map(\.persistentModelID)) { update(trips) }
}
}
@Observable @MainActor
final class MapCameraController {
private let observer: ResultsObserver<Trip, Never>
private var token: ObservationTracking.Token?
var bounds: MapCameraBounds?
init(modelContext: ModelContext) throws {
observer = try ResultsObserver<Trip, Never>(modelContext: modelContext)
token = withContinuousObservation(options: [.didSet]) { [weak self] _, _ in
self?.bounds = self?.calculateBounds(trips: self?.observer.results ?? [])
}
}
}
The important operational detail is the token. Store it for as long as the owner should keep receiving updates.
HistoryObserver
HistoryObserver is for code that cares about store-level changes, not just a current query result.
It exposes an observable eventCounter; when it changes, fetch persistent history and process the
transactions you care about.
@SyncActor
final class ServerSync {
private let observer: HistoryObserver
private var token: ObservationTracking.Token?
func start(modelContainer: ModelContainer) throws {
observer = try HistoryObserver(authors: ["App"], modelContainer: modelContainer)
token = withContinuousObservation(options: .didSet) { [weak self] _ in
_ = self?.observer.eventCounter
self?.processChanges()
}
}
}
The author filter matters for sync. If server-originated transactions are written with a different author, your upload loop can skip echoing those changes back to the server.
Adoption Checklist
sectionBy: where grouped lists are currently hand-rolled.
It reduces view state and keeps grouping aligned with the fetch definition.