Create search bar for SwiftUI List for macOS - compiler unable to type-check expression in reasonable time

Viewed 1017

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...

  1. add a new wrapped property @Binding var searchText: String to Apple's struct Filter;
  2. add a TextField() and Button into an HStack and add that into a VStack with the existing controls;

To the SwiftUI List...

  1. add a new wrapped property @Binding var searchText: String to my SwiftUI List;
  2. rewrite the conditional if statement in the List to include a check for searchText.

...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?

0 Answers
Related