Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to rerun @FetchRequest with new predicate based on user input?

I've a list displaying object from CoreData using @FetchRequest, I want to provide the user with a bar button that when clicked will filter the displayed list. How can I change the @FetchRequest predicate and rerun it dynamically to rebuild the list with the filtered items?

struct EmployeeListView : View {
    @FetchRequest(
        entity: Department.entity(),
        sortDescriptors: [NSSortDescriptor(keyPath: \Department.name, ascending: false)],
    )
    var depts: FetchedResults<Department>
    @Environment(\.managedObjectContext) var moc

    var body: some View {
        NavigationView {
            List {
                ForEach(depts, id: \.self) { dept in
                    Section(header: Text(dept.name)) {
                        ForEach(dept.employees, id: \.self) { emp in
                            Text(emp.name)
                        }
                    }
                }
            }
            .navigationBarTitle("Employees")
         }
    }

}

I know how to provide a filter, what I don't know how is changing the property wrapper predicate and rerunning the fetch request.

like image 260
M.Serag Avatar asked Sep 07 '19 12:09

M.Serag


1 Answers

You can change your results based on a binding in your fetch predicate, but with Bool vars, I've found it is difficult to do. The reason is, the predicate to test a Bool in CoreData is something like NSPredicate(format: "myAttrib == YES") whereas your Bool binding variable will be true or false, not YES or NO... So if you NSPredicate(format: "%K ==%@", #keypath(Entity.seeMe), seeMe.wrappedValue), this will always be false. Maybe I'm wrong, but this is what I've experienced.

You can filter your fetch based on String data easier.. But it works a little differently than my example below because your need to run your fetch in the init() of the View like this:

 @Binding var searchTerm:String
 var fetch: FetchRequest<Entity>
 var rows: FetchedResults<Entity>{fetch.wrappedValue}


 init(searchTerm:Binding<String>) {
   self._searchTerm = searchTerm
   self.fetch = FetchRequest(entity: Entity.entity(), sortDescriptors: [], predicate: NSPredicate(format: "%K == %@", #keyPath(Entity.attribute),searchTerm.wrappedValue))
 }

To accomplish the task you've described, clicking on a bar button item thereby toggling a Bool, the below example is what I would recommend:

This example will accomplish your goal without changing the fetch predicate. It uses logic to decide whether or not to display a row of data based on the entry in the data model and the value of your @State variable.


import SwiftUI
import CoreData
import Combine

struct ContentView: View {

    @Environment(\.managedObjectContext) var viewContext
    @State var seeMe = false

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Entity.attribute, ascending: true)],
        animation: .default)
    var rows: FetchedResults<Entity>

    var body: some View {

        NavigationView {
            VStack {


                ForEach(self.rows, id: \.self) { row in

                    Group() {
                        if (self.validate(seeMe: row.seeMe)) {
                            Text(row.attribute!)
                        }
                    }

                }
                .navigationBarItems(leading:
                    Button(action: {
                        self.seeMe.toggle()
                    }) {
                        Text("SeeMe")
                    }
                )

                Button(action: {
                    Entity.create(in: self.viewContext, attribute: "See Me item", seeMe: true)
                }) {
                    Text("add seeMe item")
                }

                Button(action: {
                    Entity.create(in: self.viewContext, attribute: "Dont See Me item", seeMe: false)
                }) {
                    Text("add NON seeMe item")
                }

            }
        }

    }

    func validate(seeMe: Bool) -> Bool {
        if (self.seeMe && seeMe) {
            return true
        } else if (!self.seeMe && !seeMe ){
            return true
        } else {
            return false
        }
    }
}


extension Entity {
    static func create(in managedObjectContext: NSManagedObjectContext,
                       attribute: String,
                       seeMe: Bool
    ){

        let newEvent = self.init(context: managedObjectContext)
        newEvent.attribute = attribute
        newEvent.seeMe = seeMe
    }

    static func save(in managedObjectContext: NSManagedObjectContext) {
        do {
            try  managedObjectContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }

}

To use this example, create a core data model with an entity named "Entity" and two attributes, one named 'attribute' as a String and the other named 'seeMe' as a Bool. Then run it, press the buttons to create the two types of data and then click the bar button item at the top to select which to display.

I'ts not the prettiest of examples, but it should demonstrate the functionality of what you are trying to accomplish.

like image 93
JerseyDevel Avatar answered Nov 03 '22 21:11

JerseyDevel