iOS 27 brought four new capabilities to SwiftData in WWDC26 session 274: sectioned query, the .codable attribute, ResultsObserver, and HistoryObserver. This post shows step by step how to use the sectioned query feature through @Query, how to store types you don't own with .codable, and how to listen to query results outside SwiftUI with ResultsObserver. All four are covered based on the WWDC26 session 274 transcript and Apple's official SwiftData docs, as they stood during the beta period (June 2026).
💡 Pro Tip: Use the.codableattribute only for types you don't own (e.g. a framework's types), not for models you define yourself — full@Modelsupport (filtering, sorting, indexing) is always more powerful for your own types.
Table of Contents
- The iOS 27 SwiftData Delta: WWDC26 Session 274
- Sectioned Query: Moving List Grouping Into the Query
- When Does Sectioned Query Pay Off?
- The .codable Attribute: Storing Types You Don't Own
- The Limits of .codable: Predicate, Sort, and Migration
- ResultsObserver: Live Query Observation Outside SwiftUI
- HistoryObserver: Persistent History and Remote Changes
- Which Constraints From "Production Lessons" Got Closed?
- Migration and Backward Compatibility
- FAQ
- What is SwiftData sectioned query and how do you use it?
- How do you store custom types in SwiftData (.codable)?
- How do you use ResultsObserver outside SwiftUI?
- Does iOS 27 SwiftData change the decision to go back to Core Data?
- Update (September 2026)
- Conclusion
- Sources
The iOS 27 SwiftData Delta: WWDC26 Session 274
Apple introduced four additions to SwiftData in WWDC26 session 274: sectioning query results, persisting custom/third-party types via Codable, observing queries anywhere outside SwiftUI with ResultsObserver, and tracking data store changes with HistoryObserver. These same four capabilities are listed in the same order on Apple's official "SwiftData updates" page; the session itself is presented as the SwiftData innovations in "Apple's 2027 releases" — the iOS 27 and macOS 27 (Golden Gate) cycle.
This post describes the API surface as it stood during the beta period (as of June 26, 2026). There's no new feature or announcement on the Core Data side in the same cycle; object-ID interop is still missing. So if you're already using SwiftData in production, these four additions directly concern you.
It's no coincidence that these four are grouped in the same session and the same "what's new" page — all of them aim to take SwiftData beyond SwiftUI, into service layers and complex list UIs.
There's no announcement in either WWDC26 session 274 or Apple's official SwiftData docs (Query macros, Predicate, Schema.Attribute.Option) about filtering #Predicate with enum values on iOS 27; this post therefore does not cover enum predicates. The four sections below cover, in order, these four additions that have a one-to-one counterpart in the session and the documentation.
Sectioned Query: Moving List Grouping Into the Query
Before iOS 27, if you wanted to section a list with SwiftData (say, grouping trips by destination city), you had to manually group the entire fetched result with Dictionary(grouping:by:) or manually populate Section views. The sectionBy parameter added to @Query moves this work directly into the query: you provide a KeyPath starting from the model's root type, and SwiftData groups the results by that value and hands you a ready-made section collection.
1import SwiftData2import SwiftUI3 4struct TripListView: View {5 @Query(sort: \Trip.startDate, sectionBy: \Trip.destination)6 private var trips: [Trip]7 8 var body: some View {9 List {10 ForEach(_trips.sections) { section in11 Section(section.id) {12 ForEach(section) { trip in13 Text(trip.name)14 }15 }16 }17 }18 }19}The .sections collection, accessed via the property wrapper's underscored name (_trips), consists of sections each carrying the value of the sectionBy KeyPath as its id. Since the grouping now comes from the query, manual grouping with Dictionary(grouping:by:) and keeping the section list in sync go away; on the SwiftUI side you just build the outer ForEach over the sections and the inner ForEach over the section itself. Because the section itself is a collection, you can hand it directly to the inner ForEach — you don't need to reach for a separate elements array.
Apple's official documentation published this API under "Additional query macros" with concrete overloads: signatures like Query(_:animation:sectionBy:) and Query(filter:sort:transaction:sectionBy:) exist. In other words, sectioning can be used in the same macro together with filtering and sorting — you don't need to learn a separate API.
When Does Sectioned Query Pay Off?
Scenario | Is sectioned query recommended? | Why |
|---|---|---|
Fixed, small number of groups (e.g. status: active/done) | Yes | Section ids come from the query, no manual sync needed |
Grouping key changes often (e.g. search results) | Use with care | The section collection is recomputed on every change |
Grouping by a .codable field | No | Apple doesn't state this explicitly; the fact that codable content is opaque to SwiftData raises the expectation that grouping isn't supported either — the safe route is keeping the grouping key in a separate native String field |
In practice, deciding whether to add sectioning comes down to three questions:
- Is the number of groups limited?: A small set of fixed values like status, category, or priority — sectioning significantly simplifies UI code.
- Is the grouping key a
String?: ThesectionByKeyPath must resolve to aStringorString?field; grouping by an enum orDatemeans adding a computedStringfield. - Used together with sorting?: The
Query(filter:sort:transaction:sectionBy:)overload combines sectioning with your existingsortparameter in one query — no separate fetch needed.
The .codable Attribute: Storing Types You Don't Own
Until now, storing a model property required either a natively supported type (String, Int, Date, small structs) or your own Codable-conforming type. The .codable option added in iOS 27 opens a third path: you can now store a Codable-conforming type you don't own (e.g. one defined by a framework) directly, without SwiftData needing to infer a schema for it.
1import MapKit2import SwiftData3 4@Model5final class Trip {6 var name: String7 var startDate: Date8 var destination: String9 10 @Attribute(.codable)11 var externalLocation: MKMapItem.Identifier?12 13 init(name: String, startDate: Date, destination: String) {14 self.name = name15 self.startDate = startDate16 self.destination = destination17 }18}Defined on Schema.Attribute.Option as static var codable: Schema.Attribute.Option { get }, this option tells SwiftData to delegate serialization to the type's own Codable implementation.
Apple positions this clearly in the WWDC26 session: .codable is not the recommended path for types you define yourself — it's an "escape hatch" for persisting types SwiftData doesn't natively support. For types you own, modeling with full @Model support (filtering, sorting, indexing) should always be preferred.
The Limits of .codable: Predicate, Sort, and Migration
The content of .codable attributes stays opaque to SwiftData — bringing two concrete limitations. In the transcript's words: "the contents of codable attributes are opaque to SwiftData... they can't be used in Predicates to filter results or for sorting." In other words, you can't filter on externalLocation inside #Predicate or sort with a SortDescriptor; these fields stay read/write only.
The second limitation is on the migration side — but this time it works in your favor: even if the shape of the codable type changes (a field added, one removed), SwiftData doesn't treat this as a migration and doesn't trigger schema versioning. The responsibility falls entirely on your Codable implementation — your encode/decode must remain forward and backward compatible.
The practical implications of these two limitations:
- Loss of filtering: For a "filter by externalLocation" search feature, you may need a separate native field (e.g. plain
String) instead of.codable. - Loss of sorting: To sort a list by a
.codablefield, duplicate the sort key into a separate native field — SwiftData won't do it for you. - Migration freedom: In exchange, you can evolve your codable payload fully independently of SwiftData's schema migration — no
VersionedSchemaneeded for a new field. - Escape-hatch discipline: Taking Apple's "escape hatch" wording seriously means using
.codableonly for types you genuinely don't own, keeping your own domain models on native@Modelfields.
1// Even if the shape of externalLocation changes, SwiftData won't trigger a migration;2// your Codable implementation must guarantee forward/backward compatibility.3struct LegacyLocationPayload: Codable {4 var identifier: String5 var displayName: String? // field added later — must stay optional in decode6}Feature | Native @Model field | .codable attribute |
|---|---|---|
Filterable with #Predicate | Yes | No |
Sortable with SortDescriptor | Yes | No |
Triggers migration on shape change | Yes | No |
Can store a type you don't own (3rd-party) | No (generally) | Yes |
ResultsObserver: Live Query Observation Outside SwiftUI
@Query has always been tied to a SwiftUI view — there was no official way to set up the same live observation outside one (in a service layer, an engine class). The ResultsObserver final class added in iOS 27 fills this gap: without depending on SwiftUI, it tracks changes to models matching your fetch criteria in real time via Swift Observation.
ResultsObserver shares the same primitive as @Query: in the non-sectioned case you pass Never as the SectionTitle type parameter, and in the sectioned case you pass a concrete type (e.g. String) along with sectionBy. To listen for changes, you use withContinuousObservation with the didSet option; this function triggers a callback on every change and returns an ObservationTracking.Token so you can control the lifetime of the observation.
1import Observation2import SwiftData3 4@MainActor5final class MapCameraController {6 private var observationToken: ObservationTracking.Token?7 private let observer: ResultsObserver<Trip, Never>8 9 init(context: ModelContext) throws {10 observer = try ResultsObserver<Trip, Never>(modelContext: context)11 observationToken = withContinuousObservation(options: [.didSet]) { [weak self] _ in12 // Outside SwiftUI, e.g. in a camera controller,13 // reposition the camera based on the current Trip list14 _ = self?.observer.results15 }16 }17}This shows ResultsObserver isn't just "@Query without SwiftUI" — it also shares the sectioning primitive: a sectioned ResultsObserver gives the same grouped results outside SwiftUI via .sections.
The doors ResultsObserver practically opens:
- Service-layer integration: A sync service or background task can now observe the current data set independently of the UI, via an official API.
- Testability: Since it needs no SwiftUI view hierarchy, it can be set up inside a unit test — verify your query observation logic without rendering a view.
- Reusing the same fetch logic: Sharing the same
FetchDescriptorandsectionBylogic between@QueryandResultsObservermeans you don't write your filtering rule twice.
HistoryObserver: Persistent History and Remote Changes
While ResultsObserver tracks results matching a specific fetch criterion, HistoryObserver answers a broader problem: how do you notice remote changes to the container's data stores (e.g. from CloudKit sync)? It's a separate, new class that automatically listens to the ModelContainer's remoteChange notifications and determines whether an incoming change is relevant.
When a relevant change is detected, the observer increments its own @Observable eventCounter property; a SwiftUI view or another observer can react by watching this eventCounter. On the performance side, HistoryObserver tracks its position in each data store's transaction history via historyTokens — so only the new transactions added since the last check are processed incrementally. You can also narrow the observation to specific model types with the observedModels parameter.
1import SwiftData2 3final class SyncStatusStore {4 private let historyObserver: HistoryObserver5 6 init(container: ModelContainer) throws {7 historyObserver = try HistoryObserver(8 observedModels: [Trip.self],9 modelContainer: container10 )11 }12 13 var hasRemoteChanges: Bool {14 historyObserver.eventCounter > 015 }16}ResultsObserver answers "which results changed", while HistoryObserver answers "did anything change anywhere in the container" — two complementary APIs operating at different levels.
Points to keep in mind with HistoryObserver:
- historyTokens work incrementally: It doesn't rescan the entire history on every check, only new transactions since the last check — cost stays low even with frequent checks.
- Narrowing observedModels affects performance: Specifying only the types you actually care about, instead of watching every model type, prevents unnecessary callback triggering.
- Central role in CloudKit sync: It's the official way to notice remote changes (from another device via CloudKit sync) — previously this needed hand-built mechanisms like
NSPersistentHistoryTracking.
Which Constraints From "Production Lessons" Got Closed?
In my post on six months with SwiftData in production, I described three scenarios where we went back to Core Data; these four iOS 27 additions only close one of them. In Scenario 3, we tracked sync state with NSPersistentHistoryToken in a cross-device offline-first app, and as I put it there, SwiftData had "no equivalent of fetchHistory(after: token). Implementing it manually is 200+ lines." HistoryObserver, with ModelContext's fetchHistory method, replaces exactly this hand-written layer. Scenario 1 (25-40% slowdown on 1.5M rows with compound predicates) and Scenario 2 (custom NSManagedObject behavior, validation hooks) remain open — sectioning, .codable, and ResultsObserver don't touch either.
That said, iOS 27 doesn't close one constraint: performance characteristics. The differences I measured in the SwiftData vs Core Data comparison don't change with these four additions — they're all API-surface and developer-experience additions; they don't touch the query engine itself. So keep relying on that post's measurements for a performance decision; this post only answers "what can you newly do."
For general SwiftData fundamentals, you can also check out our comprehensive introduction to SwiftData — sectioned query and .codable build on top of the basic @Model/@Query concepts covered there.
Migration and Backward Compatibility
The migration behavior of .codable attributes is clear from above: no migration is triggered even on shape changes, and compatibility responsibility falls on your Codable implementation. On the sectioned query side, sectionBy is a fetch-side grouping parameter, so it doesn't change the schema; Apple doesn't separately clarify the migration impact of adding or removing it. When extending an existing Codable model, the safest approach is to always define new fields as optional and handle the missing-field case in decode.
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
I've collected the items you should check before taking this post into production code into a single checklist. These are the steps most easily skipped when adding sectioned query, .codable, and ResultsObserver to a project at the same time; you can work through them one by one.
FAQ
What is SwiftData sectioned query and how do you use it?
The sectionBy parameter added to @Query in iOS/iPadOS 27 takes a KeyPath starting from the model's root type and groups the results by that value. You iterate over both the sections and the elements within each section using ForEach on the .sections collection, accessed via the property wrapper's underscored name (_trips); each section's id is the value of the sectionBy KeyPath.
How do you store custom types in SwiftData (.codable)?
To store a Codable-conforming type you don't own (e.g. one defined by a framework), you add @Attribute(.codable) to the model property. SwiftData delegates serialization to the type's own Codable implementation; in exchange, this field can't be filtered in #Predicate or sorted with SortDescriptor, and it doesn't trigger migration even if its shape changes.
How do you use ResultsObserver outside SwiftUI?
ResultsObserver is the non-SwiftUI counterpart of @Query: it supports the same filtering/sorting/sectioning primitives, but works on any code path via Swift Observation. You get a change callback by calling withContinuousObservation with the didSet option, and you keep the returned ObservationTracking.Token alive for the lifetime of the observation.
Does iOS 27 SwiftData change the decision to go back to Core Data?
No. The iOS 27 additions (sectioning, .codable, ResultsObserver, HistoryObserver) are real but targeted improvements; there's no innovation on the Core Data side in the same cycle, and object-ID interop is still missing. So it's more accurate to read iOS 27 not as a release that declares SwiftData "now definitively production-ready," but as an incremental step that closes some of the previous limitations.
Update (September 2026)
The SwiftData section of Apple's iOS & iPadOS 27 Release Notes and macOS 27 Golden Gate Release Notes pages now includes this item: a deadlock that could occur for @Query when saving a ModelContext on a background actor while new async tasks were being scheduled for a ModelActor at the same time has been fixed (bug 178113288). This is not a change to the sectioned query, .codable, ResultsObserver, or HistoryObserver API surface described in this post — it's the resolution of a concurrency bug.
Conclusion
The four additions iOS 27 brings to SwiftData — sectioned query, .codable, ResultsObserver, HistoryObserver — share a common theme: moving workarounds you had to hand-write in real apps into the official API. Layer sectioned query and .codable on top of the basics from our comprehensive introduction to SwiftData; for a performance decision, keep relying on our SwiftData vs Core Data comparison, since these additions don't change the query engine's performance. Some constraints from our production lessons post are eased; not all are closed.
This post describes the API surface as it stood during the beta period (June 2026); if you notice a behavior change after GA, don't forget to check the Update section of this post.
Sources
- WWDC26 Session 274 — SwiftData innovations (video/transcript) — the primary session where sectioned query, .codable, ResultsObserver, and HistoryObserver were introduced.
- Apple — SwiftData Updates — the page summarizing the four additions in the official "what's new" list.
- Apple — Schema.Attribute.Option.codable — the API reference for the
.codableoption. - Apple — ResultsObserver — the API reference for non-SwiftUI query observation.
- Apple — HistoryObserver — the API reference for persistent history observation.
- Apple — iOS/iPadOS 27 Release Notes — the deadlock fix in the SwiftData section (bug 178113288).
- mjtsai — SwiftData in Apple's OS 27 — an independent assessment after WWDC26, with a Core Data comparison.
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

