3

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")
    }
}
HangarRash
  • 7,314
  • 5
  • 5
  • 32
Mike Bedar
  • 632
  • 5
  • 14

2 Answers2

2

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)
}
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • Good to know that you can update the query like that, but doing so doesn't seem to be triggering an update.. I'm not sure if the Query is not being executed again or if SwiftUI is just not updating the view.. – Mike Bedar Jun 22 '23 at 14:49
  • I am not sure what you mean, I created a more complete example and it works as expected. Every time I change the date (I have only one) in the parent view my sub-view with the query gets refreshed and a different result is shown – Joakim Danielson Jun 22 '23 at 18:07
  • Also note that I forgot to mention one important thing :) and that is that my answer is a workaround since dynamic queries doesn't work for now. Answer updated. – Joakim Danielson Jun 22 '23 at 19:00
  • I found this hard to believe, so I went searching, and Apple uses this approach in their own sample code. See `BackyardSearchResults.swift` in the "Backyard Birds" sample project here: https://developer.apple.com/documentation/swiftui/backyard-birds-sample – jsadler Jun 30 '23 at 23:23
  • As of Xcode 15 beta 6, this line `@Query(FetchDescriptor) var measurements: [Measurement]`, seems to not work anymore. – Lawrence Gimenez Aug 10 '23 at 13:08
  • @LawrenceGimenez it can be shortened to `@Query var measurements: [Measurement]` and this should work – Joakim Danielson Aug 10 '23 at 13:38
  • @JoakimDanielson yes just figured it out too. Thanks! – Lawrence Gimenez Aug 10 '23 at 13:56
0

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.

ingconti
  • 10,876
  • 3
  • 61
  • 48