How to use a @FetchRequest with the new searchable modifier in SwiftUI?

Viewed 1067

Is it possible to use the new .searchable in combination with @FetchRequest?

I have a code like this:

struct FooListView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Foo.name, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Foo>

    @State var searchText = ""

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    NavigationLink(destination: FooView(Foo: item)) {
                        Text(item.wrappedName)
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .searchable(text: $searchText)
            .navigationTitle("Foos")
        }
    }
}

I would like to use the searchText to filter my FetchedResults.

3 Answers

I would not overcomplicate it with additional bindings. Why not only to use the onChange modifier on the searchText.

struct ContentView: View {
    @FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)])
    private var quakes: FetchedResults<Quake>
    
    @State private var searchText = ""
    
    var body: some View {
        List(quakes) { quake in
            QuakeRow(quake: quake)
        }
        .searchable(text: $searchText)
        .onChange(of: searchText) { newValue in
            quakes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "place CONTAINS %@", newValue)
        }
    }
}

WWDC 2021 Bring Core Data Concurrency to Swift and SwiftUI has a great example of this right around minute 21:33

https://developer.apple.com/wwdc21/10017

struct ContentView: View {
    @FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)])
    private var quakes: FetchedResults<Quake>

    @State private var searchText = ""
    var query: Binding<String> {
        Binding {
            searchText
        } set: { newValue in
            searchText = newValue
            quakes.nsPredicate = newValue.isEmpty
                           ? nil
                           : NSPredicate(format: "place CONTAINS %@", newValue)
        }
    }

    var body: some View {
        List(quakes) { quake in
            QuakeRow(quake: quake)
        }
        .searchable(text: query)
    }
}

In the other answers the predicate is lost if the View is re-init, e.g. by a parent View's body. That can be fixed by putting the predicate in an @State (in the same struct because the searchText and predicate are related) in a parent View like this:

struct ContentViewConfig {
    var searchText = "" {
        didSet {
            predicate = searchText.isEmpty ? nil : NSPredicate(format: "place CONTAINS %@", searchText)
        }
    }
    var predicate: NSPredicate?
}

struct ContentView: View {
    @State var config = ContentViewConfig()

     var body: some View {
        QuakesView(predicate: config.predicate)
        .searchable(text: $config.searchText)
    }
}

struct QuakesView: View {
    static var sortDescriptors = [SortDescriptor(\Quake.time, order: .reverse)]
    
    private var fetchRequest: FetchRequest<Quake>
    private var quakes: FetchedResults<Quake> {
        fetchRequest.wrappedValue
    }

    init(predicate: NSPredicate?) {
        fetchRequest = FetchRequest(sortDescriptors: Self.sortDescriptors, predicate: predicate)
    }

    var body: some View {
        List(quakes) { quake in
            QuakeRow(quake: quake)
        }
    }
}
Related