10

I am trying to build a search view for an existing CoreData app (simple logging app). I have all the data stored with CoreData and is fetched with the @FetchRequest:

@State private var searchPredicate: NSPredicate? = NSPredicate(format: "title contains[c] %@", "")

@FetchRequest( entity: Item.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Item.title, ascending: true)],
      predicate: NSPredicate(format: "title contains[c] %@", "h")
    ) 
var items: FetchedResults<Item>

It now only fetches the items that pass the predicate test which in this case are all the ones that contain an "h". I then display the results in a List in the body of the SearchView:

List {

  ForEach(Items) { Item in

      ListViewItem(title: Item.title!, subTitle: Item.subTitle!, createdAt: "\(Item.createdAt!)")

     }
}

I then created a new class "Searchbar" which is called in the searchview and is supposed to create a predicate based on the input of the search field and then pass it on as a Binding to the parent where then based on that predicate the correct items can be displayed.

Calling the searchbar at the top of a VStack in the searchview:

SearchBar(text: $searchText, predicate: $searchPredicate)

The Bindings change depending on the user input in the "searchBar":

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {

    text = searchText
    predicate = NSPredicate(format: "title contains[c] %@", searchText)

 }

So far so good...

The problem I have now run into is that we have a predicate that works but can't be called within the @Fetchrequest in the definitions since it would be called before its initialized.

@State private var searchPredicate: NSPredicate? = NSPredicate(format: "title contains[c] %@", "")


@FetchRequest(
    entity: Item.entity(),
    sortDescriptors: [
        NSSortDescriptor(keyPath: \Item.title, ascending: true)
    ],
    predicate: searchPredicate
) var items: FetchedResults<Item>

This gives me the error that the property initializer can't be run before self is available which is logical but makes me wonder and brings me back to my question: Is there a way to modify fetched results with a predicate after they are initialized?

I have also tried calling predicate related methods on the fetched results in the ForEach() statement but none of them seemed to have worked.

If there are any questions please do not hesitate to ask.

Jannis
  • 175
  • 10

3 Answers3

11

Is there a way to modify fetched results with a predicate after they are initialized?

Well... no, not in the way you try to do this, and even if you'd try to create it with NSFetchRequest instance, which is reference, and allows to change predicate later, that wouldn't work, because SwiftUI's FetchRequest stores copy of provided fetch request (or creates own with provided parameters)... so, no. But...

You can break apart view providing fetch request parameters with view constructing fetch request and showing result.

Here is a demo of approach (important part of it) which gives you possibility to get results with different dynamically changed predicates:

struct MasterView: View {
    @State var predicate: NSPredicate? = nil
    var body: some View {
        VStack {
            Button(action: { // button just for demo
                self.predicate = NSPredicate(format: "title contains[c] %@", "h")
            }, label: { Text("Filter") })
            ResultView(predicate: self.predicate)
        }
    }
}

struct ResultView: View {

    @FetchRequest
    var events: FetchedResults<Event>

    @Environment(\.managedObjectContext)
    var viewContext

    init(predicate: NSPredicate?) {
        let request: NSFetchRequest<Event> = Event.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \Event.timestamp, ascending: true)]
        if let predicate = predicate {
            request.predicate = predicate
        }
        _events = FetchRequest<Event>(fetchRequest: request)
    }
    
    var body: some View {
        List {
            ForEach(events, id: \.self) { event in
    ...
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
2

I decided to post the fully working version possible with the great answer provided by Asperi since I haven't found a working solution anywhere else.

struct MasterView: View {


@State var predicate: NSPredicate? = nil

@State private var searchText = ""


var body: some View {

    VStack {


        TextField("Search", text: $searchText, onEditingChanged: {_ in


            self.predicate = NSPredicate(format: "title contains[c] %@", "\(self.searchText)")

            print("THE PREDICATE: \(String(describing: self.predicate))")


            }, onCommit: {

             print("onCommit")

         }).foregroundColor(.primary)


        SearchView(predicate: self.predicate)

    }
}

}

And the SearchView:

struct SearchView: View {



@State private var searchText = ""

@Environment(\.managedObjectContext) var managedObjectContext



@FetchRequest var items: FetchedResults<Item>


init(predicate: NSPredicate?) {


    let request: NSFetchRequest<Item> = Item.fetchRequest() as! NSFetchRequest<Item>

    request.sortDescriptors = [NSSortDescriptor(keyPath: \Item.title, ascending: true)]

    if let predicate = predicate {

        request.predicate = predicate

    }

    _items = FetchRequest<Item>(fetchRequest: request)


}





var body: some View {


        VStack {



            List {
...
Jannis
  • 175
  • 10
  • Jannis, do you still have a working example somewhere? The one you posted is incomplete and I cannot manage to make it work. I wanted to learn from your example, because I am struggling few days now with making dynamic predicate in SwiftUI – mallow Dec 27 '19 at 13:13
1

As a little update to what @Asperi answered (Swift 5.4 Xcode 12.5), in the ResultView you can do this now:

var fetchRequest: FetchRequest<Event>

init(predicate: NSPredicate?) {
    fetchRequest = FetchRequest<Event>(entity: 
    Event.entity(), sortDescriptors: [NSSortDescriptor(keyPath: 
    \Event.timestamp, ascending: true)], predicate: predicate)
}

As FetchRequest takes NSPredicate? in its constructor:

public init(entity: NSEntityDescription, sortDescriptors: [NSSortDescriptor], predicate: NSPredicate? = nil, animation: Animation? = nil)
sgtpotatoe
  • 340
  • 6
  • 17