I was planning to add some search fields to my currently developed application and looking for a clever way to do it with the Core Data implementation I already had. In June 2021 Apple held their Annual World-Wide Developers Conference and introduced many changes to XCode, SwiftUI, UIKit and so on.
One of the new modifiers they promoted was .searchable() , which allowed developers to implement standard search functionality appropriate for the device and typical view. I’ve seen the WWDC video about it and I’ve seen a few articles about how to implement .searchable, but all of these implemented some clever filtering on the view to limit displayed items base don the filter selected.
However, when I use SwiftUI with CoreData, the search action should be followed by a clever reloading of Core Data objects from the .sqlite database automatically through triggering a re-rendering of the view. This looked like an interesting challenge.
I’m using a simple Core Data application building a Dictionary. The Dictionary consists of a single object, which is an Entry object. It has three attributes: word is the word to be entered, wordDescription is the definition of the word and usage is a sentence which utilizes the word, clarifying the meaning. Let's first have a look at our simple Core Data model for this application.
I will implement a view to manage a list of Entry objects, handling the display, creation of new entries, deletion of entries, editing the entries. I will concentrate on our topic of searching, thus I will not go into a lot of detail about how to display, edit etc.
I implement EntryListView to be the main view doing this. I will keep the default ContentView and will explain the reason why.
This is a simple implementation where I use a NavigationView embedding a List object and each list item will be an Entry object.
This code excerpt defines the environmental variable viewContext that corresponds to the ViewContext of Core Data, enabling all database operations through the object hierarchy it controls. It then defines a static FetchRequest that will fetch all Entry objects from the Core Data database when the view loads and adds these objects to an array named entries. You can see that the NSPredicate defined here will load all objects since it is a TRUE predicate, thus filters none out. The fetch method is what I'll show next as a shortcut to the few actions to do a fetch from the database. I developed it as an extension to the Entry class Xcode internally generates from the Core Data model we introduced earlier.
Now we can go ahead and develop the list view.
We take the array entries and loop over it with ForEach. Then each Entry is shown through a simple view called EntryView and has a link to EditEntryView if we want to edit the contents. Let us look at the main bodies of these SwiftUI views just to have an idea about what we will see.
When we run the application and enter some data, we can see something like this:
You can imagine that this list could be really long if we add a lot of data. It would be a really good idea to add the new .searchable modifier introduced in SwiftUI 3 (with iOS 14). It requires a binding to a variable which you will use as the filter text. I first thought to add a @State variable so as to somehow trigger a view refresh when that variable changed. This would work, but it is tricky with Core Data. remember that we have a static FetchRequest, which is executed when the view appears first. That does not use the filter text, so how can we enforce a reload of the data?
The solution lies in changing the fetching scheme so that the predicate is updated based on any changes in the filter text. Thus we should be dynamically changing the predicate and forcing the Core Data fetch to run again. I developed a constructor (init() method) that would do this.
This gets a filter string wordFilter from an external view (in this case I left ContentView for this purpose) and changes the predicate using this filter. I just developed a static function to do this called wordFilterPredicate. Note that the sub-expression [c] makes this a case-insensitive filter.
Let me explain why I could not use @State var wordFilter: String within EntryListView. Since I had to do this in the init function, which is called every time the view is going to be rendered/initialized by the SwiftUI engine and I can not use an internal variable in the initialiser before the init() method is run, I had to externalise it (thus move it to ContentView) and only pass it as a binding. This allows me to use the variable in the initialiser. Whenever the filter string changes, ContentView detects the change (since it is a @State variable for it) and renders itself and all the subviews, including the EntryListView. With this small trick, Core Data swiftly (no pun intended) reloads the data with the predicate filtering the relevant ones (in this case only showing Entry records with the word attribute containing the filter text, case insensitive).
When we run the application with the search filter introduced, the default search filter appears on the Navigation view. When the user types anything in that field, the view dynamically changes, showing only entries that have a word containing the filter string.
There are some details on how the default search functionality works in different devices, using the placement: parameter, but that is probably to be done in a different post.
This approach gives a clean way to use the new .searchable modifier with Core Data quite efficiently.