5

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)
            }
        }
    }
}
3
  • 1
    What do you want to happen? That is not clear. Commented Mar 25, 2022 at 13:09
  • What have you tried? What part of your attempt doesn't work? You haven't showed any code with your attempt. Please take the tour and read How to Ask to improve, edit and format your questions. There are many ways to do this. Commented Mar 25, 2022 at 13:13
  • I've provided more context in the question edit as you've requested. Commented Mar 25, 2022 at 13:20

2 Answers 2

3

I tried your project, made 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.

I also noticed some non-standard things in your code. PersistenceController should be a struct instead of an ObservableObject (see the default app template with core data checked). The use of computed bindings looks odd to me but fine 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 ?? "")
        }
    }
}
Sign up to request clarification or add additional context in comments.

6 Comments

Thank you, sir, for such a great answer! Using your method, I've refactored all of my lists that have search, filter, and sort. Code is much more concise. Search and filter live in harmony since I build out fetch requests in the views' initializers with NSCompoundPredicate. Let me explain my non-standard choices. I've made PersistenceController into a class since it makes more sense semantically. I want to explicitly model that I have only one PersistenceController in my app and that it has a distinct identity. Structure are primitive types that are copied and don't have distinct identities.
Default Core Data template also has PersistenceController modeled as a singleton with a visible initializer. The drawbacks of that pattern were discussed numerous times. Making it conform to ObservableObject allows us to access it in the environment. That makes DI much easier. Otherwise, we would have to pass it around in the initializers or use a singleton pattern. Paul Hudson teaches that in his Ultimate Portfolio App. As to the computed bindings, they come directly from Apple's Scott Perry in "Bring Core Data concurrency to Swift and SwiftUI" session from WWDC 2021.
Since before iOS 15, any dynamic fetch request configuration was only possible via an initializer, in this talk it is presented as a problem that those computed bindings solve. However, in my case doing in the old way turns out to be better.
One problem you might face is @StateObject are not init during previewing.
Thanks, I'll watch out for that. Although, I have a 2016 machine so I don't do much previewing. :) Another point that comes to mind in favor of ObservableObject is the following. The default Core Data template warns us that we need to handle the error appropriately in case of loadPersistentStores failure. And to route the error to the UI, we need @Published property that can only exist inside ObservableObject. But I'm split on this. It almost feels like leaving that fatalError makes more sense since as long as the user manipulates data in any way, the app will crash anyway.
|
-1

Because every time you select an item in the list, the view will get updated, thus cause your predicate to reset to its initial state.

Another approach would probably be using the Equatable protocol. Make your subview conforms to this protocol, set your own rules, and wrap your subview with EquatableView() on the parent view. You can refer to my other post regarding this problem: Why Does @EnvironmentObject Force Re-initialization of View?

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.