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.

June 15, 2026 6 minute read WWDC26 session 274
WWDC26 SwiftData session artwork
Apple's SwiftData session focused on sectioning, custom types, and model-store observation.

Highlights

Sectioned queries

@Query can group fetched results with sectionBy:, then expose sections through the backing query.

Codable storage

@Attribute(.codable) stores custom or third-party Codable values that SwiftData cannot model directly.

ResultsObserver

Query-style model observation now works in controllers, actors, and other non-SwiftUI state objects.

HistoryObserver

A 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.

Requires OS 27
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.

Requires OS 27
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.

Requires OS 27
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.

Requires OS 27
Fallback for OS 26

There is no direct one-object equivalent on OS 26. Keep using SwiftData persistent-history fetching from your sync pipeline, and trigger it after local saves, remote imports, app resume, or a light timer. Store the last processed history token per store so each pass only handles new transactions.

@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

Use sectionBy: where grouped lists are currently hand-rolled. It reduces view state and keeps grouping aligned with the fetch definition.
Audit Codable attributes before committing to them. If you need predicates, sorting, or migration over the fields, model the data explicitly.
Move non-view reactions to observers. Map state, sync state, and derived caches should not need hidden SwiftUI views just to observe data.

Sources