Mike Bedar
Mike Bedar

Reputation: 910

SwiftData query with dynamic properties in a View

I'm trying to figure out how to make a SwiftUI view that displays data from SwiftData using a query that includes variables passed into the view. I'm guessing that I won't be able to use the @Query syntax, but has anyone come up with a workable method to do something like this?

Do I need to abandon the @Query and just create a view model that instantiates it's own ModelContainer and ModelContext?

This code is obviously not compiling because the @Query is referencing the startDate and endDate variables, but this is what I want.

struct MyView: View {
    @Environment(\.modelContext) var modelContext

    @Query(FetchDescriptor<Measurement>(predicate: #Predicate<Measurement> {
    $0.date >= startDate && $0.date <= endDate }, sortBy: [SortDescriptor(\Measurement.date)])) var measurements: [Measurement]

    let startDate: Date = Date.distantPast
    let endDate: Date = Date.distantFuture

    var body: some View {
        Text("Help")
    }
}

Upvotes: 22

Views: 11962

Answers (5)

malhal
malhal

Reputation: 30617

Could try this QueryView and supply a computed var Query e.g.

struct QueryView<Model, Content>: View where Model: PersistentModel, Content: View {
    @Query var models: [Model]
    let content: ([Model]) -> Content
    
    init(query: Query<Model, [Model]>, @ViewBuilder content: @escaping ([Model]) -> Content) {
        self.content = content
        self._models = query
    }
    
    var body: some View {
        content(models)
    }
}

struct ContentView: View {
    var sort: [SortDescriptor<Item>] {
        [SortDescriptor(\Item.timestamp, order: ascending ? .forward : .reverse)]
    }
    
    var query: Query<Item, [Item]> {
        Query(sort: sort)
    }
    
    var body: some View {
        NavigationSplitView {
            QueryView(query: query) { items in

Upvotes: 0

djzhu
djzhu

Reputation: 1034

As I learn from hackingwithswift.com: users want to be able to set the sort order (or filter in your case) dynamically, which is not actually supported by @Query right now.

  1. Incorporate your result view (which utilizes @Query for data loading) as a subview and customize the @Query by passing properties into that subview, as suggested by @ingconti.
  2. If the query result size is manageable, consider filtering it before rendering it into the view.

Despite the potential need for unconventional code in the first option, it appears to be the more favorable approach.

Upvotes: 2

Ken Cooper
Ken Cooper

Reputation: 911

Here's a wrapper view for @Query that takes a FetchDescriptor, allowing for dynamic predicates, sort order, and limits.

struct DynamicQuery<Element: PersistentModel, Content: View>: View {
    let descriptor: FetchDescriptor<Element>
    let content: ([Element]) -> Content
    
    @Query var items: [Element]
    
    init(_ descriptor: FetchDescriptor<Element>, @ViewBuilder content: @escaping ([Element]) -> Content) {
        self.descriptor = descriptor
        self.content = content
        _items = Query(descriptor)
    }
    
    var body: some View {
        content(items)
    }
}

Example usage:

 struct MeasurementView : View {
    @State private var startDate: Date
    @State private var endDate: Date
    
    var measurementsDescriptor: FetchDescriptor<Measurement> {
        let predicate = #Predicate<Measurement> {
            $0.date >= startDate && $0.date <= endDate
        }
        return FetchDescriptor<Measurement>(predicate: predicate, sortBy: [SortDescriptor(\Measurement.date)])
    }

    var body : some View {
        DynamicQuery(measurementsDescriptor) { measurements in
            ForEach(measurements) { measurement in
               // ...
            }
        }
    }
}

Upvotes: 4

Joakim Danielson
Joakim Danielson

Reputation: 51973

You can't have a dynamic query (not yet) but a workaround is to inject in the dates (or the full predicate) into the view and create the query that way.

@Query var measurements: [Measurement]

init(startDate: Date, endDate: Date) {
    let predicate = #Predicate<Measurement> {
        $0.date >= startDate && $0.date <= endDate
    }

    _measurements = Query(filter: predicate, sort: \.date)
}

Upvotes: 29

ingconti
ingconti

Reputation: 11646

my two cents. I wrote an inner class to show filtered record in list:

struct DemoListContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query(
        FetchDescriptor()
    ) private var items: [Item]
    
    
    init(endDate: Date) {
        let past = Date.distantPast
        let predicate = #Predicate<Item> {
            ($0.creationDate ?? past) <= endDate
        }
        _items = Query(filter: predicate)
    }
    
    var body: some View {
        NavigationView {
            VStack{
                Text("\(items.count)")
                List {
                    ForEach(items) { item in
                        ItemCell(item: item)
                    }
                }
            }
        }
    }
}

it will called in:

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    
    @State var lastFetch = Date()
    
    var body: some View {
            ListContentView(endDate: lastFetch)
        }
}

Hope can help.

Upvotes: 1

Related Questions