11

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.

malhal
  • 26,330
  • 7
  • 115
  • 133
gurehbgui
  • 14,236
  • 32
  • 106
  • 178
  • 4
    [wwdc 2021 Core Data Concurrency](https://developer.apple.com/wwdc21/10017) about minute 21:33 – lorem ipsum Jul 26 '21 at 15:00
  • searchable is a view modifier for NavigationView. What do you mean by using it for FetchResults? You can use the text as an NSPredicate. – SeaSpell Jul 26 '21 at 16:32
  • @loremipsum this was exactly what I was looking for. Thank you very much. If you post this as an answer I will accept it. – gurehbgui Jul 27 '21 at 06:41
  • I'm glad it worked I added a solution – lorem ipsum Jul 27 '21 at 17:55
  • 1
    I noticed with both answers if ContentView is init again (because of a state change in a parent) the search predicate is lost even if search is still active and the full list is shown again despite there still being active search text. – malhal Jun 30 '22 at 13:50

3 Answers3

19

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)
        }
    }
}
Marc T.
  • 5,090
  • 1
  • 23
  • 40
  • 1
    Just an addition to anyone that may not know. Within the string format, "place" represents the attribute you're wanting to search through. My app kept crashing and I couldn't figure out why. So say you've named your attribute "name". You'd write "name CONTAINS %@" – Yotzin Castrejon Mar 07 '22 at 17:17
11

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)
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
1

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)
        }
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133