25

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")
    }
}
0

5 Answers 5

32

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)
}
Sign up to request clarification or add additional context in comments.

17 Comments

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..
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
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.
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: developer.apple.com/documentation/swiftui/backyard-birds-sample
This is the best solution I have found. Unfortunately, this will require odd Views to be created from time to time if you have a more complex dynamic query.
|
6

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
               // ...
            }
        }
    }
}

1 Comment

This approach is quite scalable and resolves many issues that was not provided by swift data. Nice way to synchronize view
2

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.

Comments

1

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.

Comments

1

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

1 Comment

That’s a good generic solution

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.