I want to introduce a case (and maybe diacritic) insensitive search function into a "search bar" across the top of a SwiftUI List to dynamically filter the list, however...
"The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
Using Core Data framework. Currently using Xcode 11.5, writing Swift 5.
In iOS programming I have always used a UISearchBar with the updateSearchResults(for:) method to rebuild an NSFetchedResultsController with a compound NSPredicate - in the FRC I would check...
if
searchController.isActive,
let searchText = searchController.searchBar.text,
searchText.count > 0 {
// and build `NSCompoundPredicate` here, to subsequently add to the FRC fetch request.
}
I chose this method over filtering a mutable array copy of the data set as I understood that it was/is far more efficient.
But Apple's sample code in their SwiftUI Tutorial is much different, and uses this conditional if statement...
struct LandmarkList: View {
@EnvironmentObject private var userData: UserData
@Binding var selectedLandmark: Landmark?
@Binding var filter: FilterType
var body: some View {
List(selection: $selectedLandmark) {
ForEach(userData.landmarks) { landmark in
if (!self.userData.showFavoritesOnly || landmark.isFavorite)
&& (self.filter == .all
|| self.filter.category == landmark.category
|| (self.filter.category == .featured && landmark.isFeatured)) {
LandmarkRow(landmark: landmark).tag(landmark)
}
}
}
}
}
If you've run through the tutorial and built this macOS app you'll know this works well.
So how to implement a search bar?
I read some blogs and SO Q&A.
This answer to this question "how to display a search bar with SwiftUI" had me thinking I could use NSViewRepresentable, but AppKit does not have the same (now) easy to use search tools as UIKit, so I'd be back to learning more AppKit.
The answer with the most votes to the same question is a really great solution but also involves a lot of code.
My current solution involved building on the existing Filter struct in the Apple SwiftUI tutorial:
To Apple's struct Filter...
- add a new wrapped property
@Binding var searchText: Stringto Apple's structFilter; - add a
TextField()andButtoninto anHStackand add that into aVStackwith the existing controls;
To the SwiftUI List...
- add a new wrapped property
@Binding var searchText: Stringto my SwiftUIList; - rewrite the conditional if statement in the
Listto include a check forsearchText.
...and the code...
struct Filter: View {
@EnvironmentObject private var userData: UserData
@Binding var filter: FilterType
// 1. NEW PROPERTY
@Binding var searchText: String
var body: some View {
VStack {
// 2. NEW TEXTFIELD WITH CANCEL BUTTON
HStack {
TextField("Search", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Cancel", action: {
self.searchText = String()
})
}
HStack {
Picker(selection: $filter, label: EmptyView()) {
ForEach(FilterType.allCases) { choice in
Text(choice.name).tag(choice)
}
}
Spacer()
Toggle(isOn: $appData.showFavouritesOnly) {
Text("Favourites")
}
}
}
}
}
...and...Version 1... with help from this SO answer...
var body: some View {
List(selection: $selectedItem) {
ForEach(fetchedItem) { item in
// 4. REWRITE IF STATEMENT
if (!self.userData.showFavouritesOnly || item.isFavourite)
&& (self.searchText.count == 0
...EITHER...
|| (item.name?.contains(where: { $0.compare(self.searchText, options: .caseInsensitive) == .orderedSame}) == true))
...OR...
|| (item.name?.contains(where: {$0.caseInsensitiveCompare(self.searchText) == .orderedSame}) == true))
&& (self.filter == .all
|| self.filter.name == item.category
|| (self.filter.category == .featured && item.isFeatured)) {
ItemRow(item: item).tag(item)
}
}
}
}
...and...Version 2...
var body: some View {
List(selection: $selectedItem) {
ForEach(fetchedItem) { item in
// 4. REWRITE IF STATEMENT
if (!self.userData.showFavouritesOnly || item.isFavourite)
&& (self.filter == .all
|| self.filter.name == item.category
|| (self.filter.category == .featured && item.isFeatured)) {
if self.searchText.count == 0 {
if (item.name?.contains(where: { $0.compare(self.searchText, options: .caseInsensitive) == .orderedSame}) == true) {
ItemRow(item: item).tag(item)
}
}
}
}
}
}
I receive the error message quoted at the start of this essay!
If I replace the more complicated contains(where:_) call with the following code, the "search" feature works perfectly, however is case sensitive.
(item.name?.contains(self.searchText) == true)
Any thoughts or suggestions please?