How to prevent Core Data fetch request from resetting its predicate when SwiftUI List selection changes?

Viewed 206

The sample app has a Core Data model of Garden entity that has a to-many relationship with Fruit entity. User is invited to pick fruits in the garden using a SwiftUI List in constant edit mode with a selection parameter set. The user selection will be reflected in the Core Data relationship. The issue is that when the user searches for something and then attempts to select a fruit, the search is reset. I'm assuming this is predefined behavior and wondering how to override it so the search persists i.e. the predicate that the user set via search is still active, and the list remains to be filtered.

struct FruitPicker: View {
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Fruit.name, ascending: true)],
        animation: .default)
    private var fruits: FetchedResults<Fruit>
    
    @Binding var selection: Set<Fruit>
    @State var searchText = ""
    
    var body: some View {
        List(fruits, id: \.self, selection: $selection) {
            Text($0.name ?? "")
        }
        .searchable(text: query)
        .environment(\.editMode, .constant(EditMode.active))
        .navigationTitle("Garden")
    }
    
    var query: Binding<String> {
        Binding {
            searchText
        } set: { newValue in
            searchText = newValue
            
            fruits.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", newValue)
        }
    }
}

There is a post on the Apple developer portal that goes into a somewhat similar issue. The solution that is offered in the post is to pass in a predicate and sort descriptors from the parent into the view's initializer. I've tried that and it does not solve the issue.

init(selection: Binding<Set<Fruit>>, sortDescriptors: [NSSortDescriptor], predicate: NSPredicate?) {
    _selection = selection
    let entity = Fruit.entity()
    
    _fruits = FetchRequest<Fruit>(entity: entity, sortDescriptors: sortDescriptors, predicate: predicate, animation: .default)
}
struct ContentView: View {
    @ObservedObject var garden: Garden
    
    var body: some View {
        NavigationView {
            NavigationLink("Pick Fruits in the Garden ") {
                FruitPicker(selection: $garden.fruits.setBinding(), sortDescriptors: [NSSortDescriptor(keyPath: \Fruit.name, ascending: true)], predicate: nil)
            }
        }
    }
}
1 Answers

I tried your project and put in some breakpoints and the problem is when a selection is made, ContentView's body is called, which inits FruitPicker with the default FetchRequest instead of the one with the search predicate.

In looking over your coded I noticed some non-standard things. PersistenceController should be a struct not an ObservableObject (see the default app template with core data checked). The use of computed bindings looks odd to me but cool if it works.

To fix the problem you could break up the search and the list into 2 Views, so that the List is init with the new search term and then the FetchRequest will always be correct, e.g.

struct FruitPickerSearch: View {
    @State var searchText = ""

    var body: some View {
        FruitPicker(searchText: searchText)
        .searchable(text: $searchText)
        .environment(\.editMode, .constant(EditMode.active))
        .navigationTitle("Garden")
    }
}

struct FruitPicker: View {
    private var fetchRequest: FetchRequest<Fruit>
    private var fruits: FetchedResults<Fruit> {
        fetchRequest.wrappedValue
    }
 
    init(searchText: String){
        let predicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText)
        fetchRequest = FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Fruit.name, ascending: true)],
        predicate: predicate,
        animation: .default)
    }   

    var body: some View {
        List(fruits) { fruit in
            Text(fruit.name ?? "")
        }
    }
}
Related