CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

swiftdata-queries.mdskills/swiftdata/references/

SwiftData Queries Reference

Deep reference for all @Query initializer variants, FetchDescriptor options, sort descriptors, sectioned queries, dynamic query switching, background fetch patterns, and aggregate queries.


Contents

  • @Query Initializer Variants
  • FetchDescriptor Deep Dive
  • Complex Sort Descriptors
  • Sectioned Queries Pattern
  • Dynamic Query Switching
  • Background Fetch Patterns with @ModelActor
  • Aggregate Queries
  • Enumerate for Large Datasets

@Query Initializer Variants

@Query is a SwiftUI property wrapper (DynamicProperty) that automatically fetches and observes persistent model data. All variants are @MainActor.

Basic (No Filter, No Sort)

// Fetch all, default order
@Query private var trips: [Trip]

// With animation
@Query(animation: .default) private var trips: [Trip]

// With transaction
@Query(transaction: Transaction(animation: .spring)) private var trips: [Trip]

Filter + SortDescriptor Array

@Query(
    filter: #Predicate<Trip> { $0.isFavorite == true },
    sort: [SortDescriptor(\.startDate, order: .reverse)]
)
private var favoriteTrips: [Trip]

// With animation
@Query(
    filter: #Predicate<Trip> { $0.isFavorite == true },
    sort: [SortDescriptor(\.startDate, order: .reverse)],
    animation: .default
)
private var favoriteTrips: [Trip]

Filter + KeyPath Sort

@Query(
    filter: #Predicate<Trip> { $0.destination != "" },
    sort: \.startDate,
    order: .forward
)
private var upcomingTrips: [Trip]

// With optional key path sort
@Query(
    sort: \.endDate,  // KeyPath<Trip, Date?> -- optional sort key
    order: .reverse
)
private var tripsByEndDate: [Trip]

FetchDescriptor

static var recentDescriptor: FetchDescriptor<Trip> {
    let now = Date()
    var d = FetchDescriptor<Trip>(
        predicate: #Predicate { $0.startDate > now },
        sortBy: [SortDescriptor(\.startDate)]
    )
    d.fetchLimit = 10
    return d
}

@Query(RecentTripsView.recentDescriptor) private var recentTrips: [Trip]

// With animation
@Query(RecentTripsView.recentDescriptor, animation: .default)
private var recentTrips: [Trip]

Query Properties

PropertyTypeDescription
wrappedValueResult (typically [Element])Most recent fetched results
modelContextModelContextThe context used for fetching
fetchError(any Error)?Error from most recent fetch, if any

Access fetchError to detect query failures:

struct TripListView: View {
    @Query private var trips: [Trip]

    var body: some View {
        Group {
            if let error = $trips.fetchError {
                ContentUnavailableView("Fetch Error",
                    systemImage: "exclamationmark.triangle",
                    description: Text(error.localizedDescription))
            } else {
                List(trips) { trip in
                    Text(trip.name)
                }
            }
        }
    }
}

FetchDescriptor Deep Dive

Full Property Reference

var descriptor = FetchDescriptor<Trip>()
PropertyTypeDefaultDescription
predicatePredicate<T>?nilFilter condition
sortBy[SortDescriptor<T>][]Sort order
fetchLimitInt?nilMaximum results
fetchOffsetInt?nilSkip first N results
includePendingChangesBooltrueInclude unsaved in-memory changes
propertiesToFetch[PartialKeyPath<T>]allSpecific properties to load
relationshipKeyPathsForPrefetching[PartialKeyPath<T>][]Related models to eagerly load

fetchLimit and fetchOffset (Pagination)

func fetchPage(page: Int, pageSize: Int) throws -> [Trip] {
    var descriptor = FetchDescriptor<Trip>(
        sortBy: [SortDescriptor(\.startDate)]
    )
    descriptor.fetchLimit = pageSize
    descriptor.fetchOffset = page * pageSize
    return try modelContext.fetch(descriptor)
}

includePendingChanges

When true (default), the fetch includes objects inserted or modified in the current context but not yet saved. Set to false for read-only queries against the persisted store only.

var descriptor = FetchDescriptor<Trip>()
descriptor.includePendingChanges = false  // Only persisted data

Note: includePendingChanges: true cannot be used with batched fetches (fetch(_:batchSize:)). This will throw SwiftDataError.includePendingChangesWithBatchSize.

propertiesToFetch (Partial Loading)

Load only specific attributes to reduce memory footprint:

var descriptor = FetchDescriptor<Trip>()
descriptor.propertiesToFetch = [\.name, \.destination, \.startDate]
let trips = try modelContext.fetch(descriptor)
// Only name, destination, startDate are loaded; other properties fault on access

relationshipKeyPathsForPrefetching

Eagerly load related objects to avoid N+1 query patterns:

var descriptor = FetchDescriptor<Trip>()
descriptor.relationshipKeyPathsForPrefetching = [
    \.accommodation,
    \.tags
]
let trips = try modelContext.fetch(descriptor)
// Accessing trip.accommodation does not trigger a separate fetch

Fetch Variants on ModelContext

MethodReturnsUse Case
fetch(_:)[T]Standard fetch, all results in memory
fetch(_:batchSize:)FetchResultsCollection<T>Lazy batched loading
fetchCount(_:)IntCount only, no objects loaded
fetchIdentifiers(_:)[PersistentIdentifier]IDs only, lightweight
fetchIdentifiers(_:batchSize:)FetchResultsCollection<PersistentIdentifier>Batched ID loading
enumerate(_:batchSize:...)VoidProcess large sets in batches

FetchResultsCollection (Batched Fetch)

let results: FetchResultsCollection<Trip> = try modelContext.fetch(
    FetchDescriptor<Trip>(sortBy: [SortDescriptor(\.name)]),
    batchSize: 100
)

// Iterate lazily -- only 100 objects in memory at a time
for trip in results {
    print(trip.name)
}

Requirements for batched fetch:

  • includePendingChanges must be false (or will throw).
  • Results are read-only snapshots.

Complex Sort Descriptors

Single Sort

SortDescriptor(\.name, order: .forward)    // A-Z
SortDescriptor(\.name, order: .reverse)    // Z-A
SortDescriptor(\.startDate)                // Ascending (default)

Multi-Level Sort

let descriptor = FetchDescriptor<Trip>(
    sortBy: [
        SortDescriptor(\.isFavorite, order: .reverse),  // Favorites first
        SortDescriptor(\.startDate, order: .forward),    // Then by date
        SortDescriptor(\.name, order: .forward)          // Then alphabetical
    ]
)

Optional Key Path Sort

Sort on optional properties -- nil values sort to the end:

@Query(sort: \.endDate, order: .reverse) private var trips: [Trip]
// endDate is Date? -- trips without endDate appear last

Dynamic Sort Switching

struct TripListView: View {
    @State private var sortOrder: SortOrder = .forward
    @State private var sortKey: TripSortKey = .name

    var body: some View {
        SortedTripList(sortKey: sortKey, sortOrder: sortOrder)
            .toolbar {
                Picker("Sort", selection: $sortKey) {
                    Text("Name").tag(TripSortKey.name)
                    Text("Date").tag(TripSortKey.date)
                    Text("Destination").tag(TripSortKey.destination)
                }
            }
    }
}

enum TripSortKey: String, CaseIterable {
    case name, date, destination
}

struct SortedTripList: View {
    @Query private var trips: [Trip]

    init(sortKey: TripSortKey, sortOrder: SortOrder) {
        let sortDescriptor: SortDescriptor<Trip> = switch sortKey {
        case .name: SortDescriptor(\.name, order: sortOrder)
        case .date: SortDescriptor(\.startDate, order: sortOrder)
        case .destination: SortDescriptor(\.destination, order: sortOrder)
        }
        _trips = Query(sort: [sortDescriptor])
    }

    var body: some View {
        List(trips) { trip in
            TripRow(trip: trip)
        }
    }
}

Sectioned Queries Pattern

Current Apple docs include sectionBy: Query initializers for sectioned queries in iOS 27 / Xcode 27 beta. Use them only when the deployment target and SDK support those beta APIs.

For iOS 26-compatible guidance, custom grouping, or section keys derived from formatters/business logic, build sectioned views manually:

Using Dictionary Grouping

struct SectionedTripListView: View {
    @Query(sort: \.startDate) private var trips: [Trip]

    private var sections: [(String, [Trip])] {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMMM yyyy"
        let grouped = Dictionary(grouping: trips) { trip in
            formatter.string(from: trip.startDate)
        }
        return grouped.sorted { $0.key < $1.key }
    }

    var body: some View {
        List {
            ForEach(sections, id: \.0) { section, trips in
                Section(section) {
                    ForEach(trips) { trip in
                        TripRow(trip: trip)
                    }
                }
            }
        }
    }
}

Using Enum-Based Sections

enum TripStatus: String, CaseIterable {
    case upcoming = "Upcoming"
    case current = "Current"
    case past = "Past"
}

struct StatusSectionedView: View {
    @Query(sort: \.startDate) private var trips: [Trip]

    private func trips(for status: TripStatus) -> [Trip] {
        let now = Date()
        return trips.filter { trip in
            switch status {
            case .upcoming: trip.startDate > now
            case .current: trip.startDate <= now && trip.endDate >= now
            case .past: trip.endDate < now
            }
        }
    }

    var body: some View {
        List {
            ForEach(TripStatus.allCases, id: \.self) { status in
                let sectionTrips = trips(for: status)
                if !sectionTrips.isEmpty {
                    Section(status.rawValue) {
                        ForEach(sectionTrips) { trip in
                            TripRow(trip: trip)
                        }
                    }
                }
            }
        }
    }
}

Dynamic Query Switching

Filter + Sort Controlled by Parent

struct TripBrowserView: View {
    @State private var searchText = ""
    @State private var showFavoritesOnly = false

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

struct FilteredTripList: View {
    @Query private var trips: [Trip]

    init(searchText: String, favoritesOnly: Bool) {
        let predicate = #Predicate<Trip> { trip in
            (searchText.isEmpty || trip.name.localizedStandardContains(searchText))
            && (!favoritesOnly || trip.isFavorite == true)
        }
        _trips = Query(
            filter: predicate,
            sort: [SortDescriptor(\.startDate, order: .reverse)]
        )
    }

    var body: some View {
        List(trips) { trip in
            NavigationLink(trip.name) {
                TripDetailView(trip: trip)
            }
        }
    }
}

Full Dynamic Descriptor

struct AdvancedTripList: View {
    @Query private var trips: [Trip]

    init(
        destination: String?,
        minDate: Date?,
        sortKey: KeyPath<Trip, some Comparable>,
        ascending: Bool,
        limit: Int?
    ) {
        var descriptor = FetchDescriptor<Trip>(
            sortBy: [SortDescriptor(\.startDate, order: ascending ? .forward : .reverse)]
        )

        if let destination {
            descriptor.predicate = #Predicate<Trip> { trip in
                trip.destination == destination
            }
        }

        if let limit {
            descriptor.fetchLimit = limit
        }

        _trips = Query(descriptor)
    }

    var body: some View {
        List(trips) { trip in
            TripRow(trip: trip)
        }
    }
}

Background Fetch Patterns with @ModelActor

Basic Background Fetch

@ModelActor
actor TripDataHandler {
    func fetchUpcomingTrips() throws -> [PersistentIdentifier] {
        let now = Date()
        let descriptor = FetchDescriptor<Trip>(
            predicate: #Predicate { $0.startDate > now },
            sortBy: [SortDescriptor(\.startDate)]
        )
        return try modelContext.fetchIdentifiers(descriptor)
    }

    func fetchTripCount(destination: String) throws -> Int {
        let descriptor = FetchDescriptor<Trip>(
            predicate: #Predicate<Trip> { trip in
                trip.destination == destination
            }
        )
        return try modelContext.fetchCount(descriptor)
    }
}

// Usage from SwiftUI view
struct TripDashboardView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var upcomingCount = 0

    var body: some View {
        Text("Upcoming: \(upcomingCount)")
            .task {
                let handler = TripDataHandler(
                    modelContainer: modelContext.container
                )
                upcomingCount = (try? await handler.fetchTripCount(
                    destination: "Paris"
                )) ?? 0
            }
    }
}

Background Import with Progress

@ModelActor
actor ImportHandler {
    func importTrips(
        _ records: [TripRecord],
        progress: @Sendable (Int) -> Void
    ) throws -> Int {
        var imported = 0
        for (index, record) in records.enumerated() {
            let trip = Trip(
                name: record.name,
                destination: record.destination,
                startDate: record.startDate,
                endDate: record.endDate
            )
            modelContext.insert(trip)
            imported += 1

            // Save periodically to flush memory
            if index % 500 == 0 {
                try modelContext.save()
                progress(imported)
            }
        }
        try modelContext.save()
        return imported
    }
}

Resolving Identifiers on MainActor

@ModelActor
actor DataHandler {
    func findDuplicateIDs() throws -> [PersistentIdentifier] {
        // Complex logic to find duplicates
        let all = try modelContext.fetch(FetchDescriptor<Trip>())
        var seen = Set<String>()
        var duplicateIDs: [PersistentIdentifier] = []
        for trip in all {
            if seen.contains(trip.name) {
                duplicateIDs.append(trip.persistentModelID)
            }
            seen.insert(trip.name)
        }
        return duplicateIDs
    }
}

// On MainActor, resolve IDs to objects
struct DuplicateReviewView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var duplicates: [Trip] = []

    var body: some View {
        List(duplicates) { trip in
            Text(trip.name)
        }
        .task {
            let handler = DataHandler(modelContainer: modelContext.container)
            let ids = (try? await handler.findDuplicateIDs()) ?? []
            duplicates = ids.compactMap { id in
                modelContext.registeredModel(for: id) as Trip?
                    ?? (try? modelContext.model(for: id) as? Trip)
            }
        }
    }
}

Aggregate Queries

SwiftData does not provide built-in aggregate functions (SUM, AVG, etc.). Compute aggregates using fetch + Swift computation.

Count

let count = try modelContext.fetchCount(
    FetchDescriptor<Trip>(predicate: #Predicate { $0.isFavorite == true })
)

Sum, Average, Min, Max

let trips = try modelContext.fetch(FetchDescriptor<Trip>())

let totalBudget = trips.reduce(0.0) { $0 + $1.budget }
let averageBudget = trips.isEmpty ? 0 : totalBudget / Double(trips.count)
let maxBudget = trips.map(\.budget).max() ?? 0
let minBudget = trips.map(\.budget).min() ?? 0

Efficient Aggregates with Partial Fetch

Fetch only the property needed for aggregation:

var descriptor = FetchDescriptor<Trip>()
descriptor.propertiesToFetch = [\.budget]
let trips = try modelContext.fetch(descriptor)
let total = trips.reduce(0.0) { $0 + $1.budget }

Background Aggregate Computation

@ModelActor
actor StatsHandler {
    struct TripStats: Sendable {
        let totalCount: Int
        let favoriteCount: Int
        let averageDuration: TimeInterval
    }

    func computeStats() throws -> TripStats {
        let allTrips = try modelContext.fetch(FetchDescriptor<Trip>())
        let favoriteCount = try modelContext.fetchCount(
            FetchDescriptor<Trip>(predicate: #Predicate { $0.isFavorite == true })
        )

        let totalDuration = allTrips.reduce(0.0) { sum, trip in
            sum + trip.endDate.timeIntervalSince(trip.startDate)
        }
        let avgDuration = allTrips.isEmpty ? 0 : totalDuration / Double(allTrips.count)

        return TripStats(
            totalCount: allTrips.count,
            favoriteCount: favoriteCount,
            averageDuration: avgDuration
        )
    }
}

Enumerate for Large Datasets

Use enumerate instead of fetch when processing many records to keep memory usage constant:

// Process all trips without loading all into memory
try modelContext.enumerate(
    FetchDescriptor<Trip>(sortBy: [SortDescriptor(\.startDate)]),
    batchSize: 1000,
    allowEscapingMutations: false
) { trip in
    // Process each trip
    trip.isProcessed = true
}
try modelContext.save()

Parameters:

  • batchSize: Objects per batch (default 5000). Lower values use less memory.
  • allowEscapingMutations: When false, objects are autoreleased after the block. Set to true only if mutations must persist beyond the block.

skills

.mcp.json

README.md

tile.json