# SwiftData queries

Examples use this model:

```swift
import SwiftData
import SwiftUI

@Model
final class Workout {
    var title: String
    var startedAt: Date
    var finishedAt: Date?
    var minutes: Int
    var calories: Double
    var isFavorite: Bool = false

    init(title: String, startedAt: Date, minutes: Int, calories: Double) {
        self.title = title
        self.startedAt = startedAt
        self.minutes = minutes
        self.calories = calories
    }
}
```

## Contents

- [@Query initializers](#query-initializers)
- [FetchDescriptor in depth](#fetchdescriptor-in-depth)
- [Sorting](#sorting)
- [Grouping results into sections](#grouping-results-into-sections)
- [Changing the query at runtime](#changing-the-query-at-runtime)
- [Fetching on a background actor](#fetching-on-a-background-actor)
- [Aggregates](#aggregates)
- [enumerate for large sets](#enumerate-for-large-sets)

## @Query initializers

`@Query` is a SwiftUI `DynamicProperty`, and every initializer is `@MainActor`.

```swift
struct QueryShapes: View {
    // Everything, default order
    @Query private var all: [Workout]

    // Animate changes
    @Query(animation: .snappy) private var animated: [Workout]
    @Query(transaction: Transaction(animation: .easeOut)) private var viaTransaction: [Workout]

    // Filter plus an array of sort descriptors
    @Query(filter: #Predicate<Workout> { $0.minutes >= 30 },
           sort: [SortDescriptor(\Workout.startedAt, order: .reverse)],
           animation: .default)
    private var long: [Workout]

    // Filter plus a single key path; optional keys are allowed
    @Query(filter: #Predicate<Workout> { $0.isFavorite },
           sort: \Workout.finishedAt, order: .forward)
    private var favorites: [Workout]

    // A prepared descriptor
    @Query(QueryShapes.lastTen) private var recent: [Workout]
    @Query(QueryShapes.lastTen, animation: .default) private var recentAnimated: [Workout]

    static var lastTen: FetchDescriptor<Workout> {
        var d = FetchDescriptor<Workout>(sortBy: [SortDescriptor(\.startedAt, order: .reverse)])
        d.fetchLimit = 10
        return d
    }

    var body: some View { Text("\(all.count)") }
}
```

A query exposes its results as `wrappedValue`, the `modelContext` it reads
from, and `fetchError: (any Error)?` for a fetch that failed. The wrapper has
no projected value, so reach it through the underscored `_workouts`. Surface
that error instead of showing an empty list:

```swift
struct WorkoutHistory: View {
    @Query(sort: \Workout.startedAt, order: .reverse) private var workouts: [Workout]

    var body: some View {
        if let error = _workouts.fetchError {
            ContentUnavailableView("Could not load workouts",
                                   systemImage: "exclamationmark.triangle",
                                   description: Text(error.localizedDescription))
        } else {
            List(workouts) { Text($0.title) }
        }
    }
}
```

## FetchDescriptor in depth

| Property | Type | Default |
|---|---|---|
| `predicate` | `Predicate<T>?` | `nil` |
| `sortBy` | `[SortDescriptor<T>]` | `[]` |
| `fetchLimit` | `Int?` | `nil` |
| `fetchOffset` | `Int?` | `nil` |
| `includePendingChanges` | `Bool` | `true` |
| `propertiesToFetch` | `[PartialKeyPath<T>]` | every attribute |
| `relationshipKeyPathsForPrefetching` | `[PartialKeyPath<T>]` | `[]` |

Paging needs a stable sort, otherwise rows move between pages:

```swift
func page(_ index: Int, size: Int, in context: ModelContext) throws -> [Workout] {
    var d = FetchDescriptor<Workout>(sortBy: [
        SortDescriptor(\.startedAt, order: .reverse),
        SortDescriptor(\.title)
    ])
    d.fetchLimit = size
    d.fetchOffset = index * size
    return try context.fetch(d)
}
```

- `includePendingChanges` is `true` by default: results include inserts and
  edits in this context that are not saved yet. Set it to `false` to read only
  what is on disk.
- Batched fetches need `includePendingChanges = false`. Calling
  `fetch(_:batchSize:)` while it is still `true` (the default) throws
  `SwiftDataError.includePendingChangesWithBatchSize`.
- `propertiesToFetch` loads only the listed attributes; any other attribute is
  loaded when first touched.
- `relationshipKeyPathsForPrefetching` loads the named relationships up front,
  which avoids one extra fetch per row.

Fetch calls on `ModelContext`:

| Call | Returns |
|---|---|
| `fetch(_:)` | `[T]` |
| `fetch(_:batchSize:)` | `FetchResultsCollection<T>` |
| `fetchCount(_:)` | `Int` |
| `fetchIdentifiers(_:)` | `[PersistentIdentifier]` |
| `fetchIdentifiers(_:batchSize:)` | `FetchResultsCollection<PersistentIdentifier>` |
| `enumerate(_:batchSize:allowEscapingMutations:block:)` | `Void` |

A batched collection is walked lazily, keeping about one batch in memory at a
time. Treat its elements as read-only snapshots.

```swift
var d = FetchDescriptor<Workout>()
d.includePendingChanges = false
let batched = try context.fetch(d, batchSize: 200)
let totalMinutes = batched.reduce(0) { $0 + $1.minutes }
```

## Sorting

`SortDescriptor(\.key)` sorts ascending; pass `order: .reverse` for
descending. Several descriptors sort by the first, then break ties with the
next:

```swift
let ordering = [
    SortDescriptor(\Workout.calories, order: .reverse),
    SortDescriptor(\Workout.startedAt, order: .reverse),
    SortDescriptor(\Workout.title)
]
```

A `Bool` property cannot be a sort key: `SortDescriptor` needs a `Comparable`
value and `Bool` is not one. Sorting on an optional key works, but do not rely on where `nil` rows land;
with the SQLite store, `nil` sorts before other values in ascending order.
Add a tie-breaking descriptor or filter out `nil` when placement matters.

Let the user pick the sort by rebuilding the query in a child view:

```swift
enum WorkoutSortKey: String, CaseIterable { case date, duration, title }

struct WorkoutBrowser: View {
    @State private var key: WorkoutSortKey = .date
    @State private var order: SortOrder = .reverse

    var body: some View {
        SortedWorkouts(key: key, order: order)
            .toolbar {
                Picker("Sort", selection: $key) {
                    ForEach(WorkoutSortKey.allCases, id: \.self) { Text($0.rawValue) }
                }
            }
    }
}

struct SortedWorkouts: View {
    @Query private var workouts: [Workout]

    init(key: WorkoutSortKey, order: SortOrder) {
        let descriptor: SortDescriptor<Workout>
        switch key {
        case .date: descriptor = SortDescriptor(\.startedAt, order: order)
        case .duration: descriptor = SortDescriptor(\.minutes, order: order)
        case .title: descriptor = SortDescriptor(\.title, order: order)
        }
        _workouts = Query(sort: [descriptor])
    }

    var body: some View { List(workouts) { Text($0.title) } }
}
```

## Grouping results into sections

There is no sectioned fetch comparable to `NSFetchedResultsController`
sections. Group the query results yourself.

By a derived key:

```swift
struct WorkoutsByWeek: View {
    @Query(sort: \Workout.startedAt, order: .reverse) private var workouts: [Workout]

    private var weeks: [(label: String, items: [Workout])] {
        let grouped = Dictionary(grouping: workouts) { workout in
            workout.startedAt.formatted(.dateTime.year().week())
        }
        return grouped
            .map { (label: $0.key, items: $0.value) }
            .sorted { ($0.items.first?.startedAt ?? .distantPast) > ($1.items.first?.startedAt ?? .distantPast) }
    }

    var body: some View {
        List {
            ForEach(weeks, id: \.label) { week in
                Section(week.label) {
                    ForEach(week.items) { Text($0.title) }
                }
            }
        }
    }
}
```

By a fixed set of buckets, skipping empty ones:

```swift
enum WorkoutPhase: CaseIterable {
    case planned, running, done

    func includes(_ w: Workout, at now: Date) -> Bool {
        switch self {
        case .planned: w.startedAt > now
        case .running: w.startedAt <= now && w.finishedAt == nil
        case .done: w.finishedAt != nil
        }
    }
}

struct WorkoutsByPhase: View {
    @Query(sort: \Workout.startedAt) private var workouts: [Workout]

    var body: some View {
        let now = Date()
        List {
            ForEach(WorkoutPhase.allCases, id: \.self) { phase in
                let items = workouts.filter { phase.includes($0, at: now) }
                if !items.isEmpty {
                    Section(String(describing: phase)) {
                        ForEach(items) { Text($0.title) }
                    }
                }
            }
        }
    }
}
```

## Changing the query at runtime

The parent owns the inputs; the child turns them into a query in `init`.

```swift
struct WorkoutSearch: View {
    @State private var text = ""
    @State private var favoritesOnly = false

    var body: some View {
        NavigationStack {
            FilteredWorkouts(text: text, favoritesOnly: favoritesOnly)
                .searchable(text: $text)
                .toolbar { Toggle("Favorites", isOn: $favoritesOnly) }
        }
    }
}

struct FilteredWorkouts: View {
    @Query private var workouts: [Workout]

    init(text: String, favoritesOnly: Bool) {
        _workouts = Query(
            filter: #Predicate<Workout> {
                (text.isEmpty || $0.title.localizedStandardContains(text))
                    && (!favoritesOnly || $0.isFavorite)
            },
            sort: \.startedAt, order: .reverse
        )
    }

    var body: some View { List(workouts) { Text($0.title) } }
}
```

When more than the filter varies, build a whole descriptor:

```swift
struct RecentWorkouts: View {
    @Query private var workouts: [Workout]

    init(since start: Date?, limit: Int?) {
        var d = FetchDescriptor<Workout>(sortBy: [SortDescriptor(\.startedAt, order: .reverse)])
        if let start {
            d.predicate = #Predicate { $0.startedAt >= start }
        }
        if let limit {
            d.fetchLimit = limit
        }
        _workouts = Query(d)
    }

    var body: some View { List(workouts) { Text($0.title) } }
}
```

## Fetching on a background actor

Actor methods return `Sendable` results, such as identifiers or counts, never
model objects.

```swift
@ModelActor
actor WorkoutStore {
    func idsOfLongWorkouts(since start: Date) throws -> [PersistentIdentifier] {
        let d = FetchDescriptor<Workout>(
            predicate: #Predicate { $0.startedAt >= start && $0.minutes >= 60 }
        )
        return try modelContext.fetchIdentifiers(d)
    }

    func favoriteCount() throws -> Int {
        try modelContext.fetchCount(FetchDescriptor<Workout>(predicate: #Predicate { $0.isFavorite }))
    }

    func importSessions(_ rows: [SessionRow], progress: @Sendable (Int) -> Void) throws {
        for (offset, row) in rows.enumerated() {
            modelContext.insert(Workout(title: row.title, startedAt: row.start,
                                        minutes: row.minutes, calories: row.calories))
            if offset.isMultiple(of: 500) && offset > 0 {
                try modelContext.save()
                progress(offset)
            }
        }
        try modelContext.save()
        progress(rows.count)
    }
}

struct SessionRow: Sendable {
    let title: String
    let start: Date
    let minutes: Int
    let calories: Double
}
```

Saving every 500 rows keeps memory flat during a long import. Call the actor
from a view with the view's container:

```swift
struct FavoriteBadge: View {
    @Environment(\.modelContext) private var context
    @State private var count = 0

    var body: some View {
        Text("\(count)")
            .task {
                let store = WorkoutStore(modelContainer: context.container)
                count = (try? await store.favoriteCount()) ?? 0
            }
    }
}
```

Turning identifiers back into models on the main context: check the objects the
context already holds, and fetch by identifier otherwise.

```swift
@MainActor
func resolve(_ ids: [PersistentIdentifier], in context: ModelContext) throws -> [Workout] {
    try ids.compactMap { id in
        if let cached: Workout = context.registeredModel(for: id) {
            return cached
        }
        var d = FetchDescriptor<Workout>(predicate: #Predicate { $0.persistentModelID == id })
        d.fetchLimit = 1
        return try context.fetch(d).first
    }
}
```

`context.model(for:)` also exists, but it returns an unchecked instance that
fails on first access if the row was deleted; the fetch above returns `nil`
instead.

## Aggregates

SwiftData has no SUM, AVG, MIN or MAX. Fetch what you need and compute in
Swift; use `fetchCount` for counts. Restrict `propertiesToFetch` so only the
column being aggregated is loaded.

```swift
func calorieStats(in context: ModelContext) throws -> (total: Double, average: Double, peak: Double?) {
    var d = FetchDescriptor<Workout>()
    d.propertiesToFetch = [\.calories]
    let values = try context.fetch(d).map(\.calories)
    let total = values.reduce(0, +)
    let average = values.isEmpty ? 0 : total / Double(values.count)
    return (total, average, values.max())
}
```

Heavy statistics belong on a `@ModelActor` that returns a `Sendable` struct:

```swift
struct WeeklySummary: Sendable {
    let sessions: Int
    let minutes: Int
    let longest: Int
}

extension WorkoutStore {
    func summary(from start: Date) throws -> WeeklySummary {
        let d = FetchDescriptor<Workout>(predicate: #Predicate { $0.startedAt >= start })
        let rows = try modelContext.fetch(d)
        return WeeklySummary(sessions: rows.count,
                             minutes: rows.reduce(0) { $0 + $1.minutes },
                             longest: rows.map(\.minutes).max() ?? 0)
    }
}
```

## enumerate for large sets

For big sets, `enumerate` keeps memory flat where `fetch` would load
everything. Save when the loop is done.

```swift
try context.enumerate(
    FetchDescriptor<Workout>(),
    batchSize: 1_000,
    allowEscapingMutations: false
) { workout in
    workout.calories = (workout.calories * 10).rounded() / 10
}
try context.save()
```

- `batchSize` defaults to 5,000; a smaller batch uses less memory.
- With `allowEscapingMutations: false`, objects are released after each batch.
  Set it to `true` only when changes must survive past the block.
