Marcos Tanaka
Marcos Tanaka

Reputation: 3336

How to search a Table using SwiftUI on macOS?

In SwiftUI on iOS and iPadOS 15, we can add a search bar to filter a list using the searchable modifier:

struct ContentView: View {

    @Environment(\.managedObjectContext) private var viewContext

    @State private var searchTerm = ""
    @State private var selection = Set<Video.ID>()

    private var fetchRequest: FetchRequest<Video>

    private var searchResults: [Video] {
        if searchTerm.isEmpty {
            return fetchRequest.wrappedValue.filter { _ in true }
        } else {
            return fetchRequest.wrappedValue.filter { $0.matching(searchTerm) }
        }
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(searchResults) { item in
                    VideoListCellView(video: item)
                }
            }.searchable(text: $searchTerm, prompt: "Video name") // <-- HERE
        }
    }

}

However, on macOS, the searchable modifier is not supported in the new Table container:

struct ContentView: View {

    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(sortDescriptors: [SortDescriptor(\.addDate, order: .reverse)], animation: .default)
    private var videos: FetchedResults<Video>

    @State
    private var selection = Set<Video.ID>()

    var body: some View {
        NavigationView {
            Table(videos, selection: $selection, sortOrder: $videos.sortDescriptors) {
                TableColumn("Title") {
                    Text($0.title)
                }

                TableColumn("Added") {
                    Text($0.addDate)
                }.width(120)

                TableColumn("Published") {
                    Text($0.publishedAt)
                }.width(120)

                TableColumn("Duration") {
                    Text($0.duration)
                }.width(50)
            }.searchable(text: $searchTerm, prompt: "Video name") // <-- GENERATES ERROR
        }
    }

}

Trying to use it generates a compile error in the var body: some View:

The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions

Is there another way to search a Table on macOS, or is this feature not supported yet?

Upvotes: 4

Views: 1697

Answers (2)

Joakim Danielson
Joakim Danielson

Reputation: 51945

You can solve this by updating the predicate of the fetch request using a specific Binding variable.

The below solution is based on an example from the 2021 WWDC video Bring Core Data concurrency to Swift and SwiftUI where it was used on a List which is what I also used it for but I tested it on one of my tables and it works equally well.

@State private var searchText: String = ""
var query: Binding<String> {
    Binding {
        searchText
    } set: { newValue in
        searchText = newValue
        if newValue.isEmpty {
            videos.nsPredicate = NSPredicate(value: true)
        } else {
            videos.nsPredicate = NSPredicate(format: "name BEGINSWITH[c] %@", newValue)
        }
    }
}

And then you use pass this variable to .searchable

Table(videos, selection: $selection, sortOrder: $videos.sortDescriptors) {
    // ...
}
.searchable(text: query, prompt: "Search instrument")

The downside of this solution is that a new fetch request is executed for each typed letter. I tried a quick fix by adding if newValue.count < 3 { return } in the else of the query set method and it works but it might be a bad restriction, maybe something more advanced can be implemented by using Combine.

Upvotes: 0

Marcos Tanaka
Marcos Tanaka

Reputation: 3336

The solution was to add the .searchable modifier to the NavigationView instead of the Table, as Scott suggested:

struct ContentView: View {

    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(sortDescriptors: [SortDescriptor(\.addDate, order: .reverse)], animation: .default)
    private var videos: FetchedResults<Video>

    @State private var selection = Set<Video.ID>()
    @State private var searchTerm = ""

    private var searchResults: [Video] {
        if searchTerm.isEmpty {
            return videos.filter { _ in true }
        } else {
            return videos.filter { $0.matching(searchTerm) }
        }
    }

    var body: some View {
        NavigationView {
            Table(searchResults, selection: $selection, sortOrder: $videos.sortDescriptors) {
                TableColumn("Title", value: \.title) {
                    Text($0.title)
                }

                TableColumn("Added", value: \.addDate) {
                    Text($0.addDate)
                }.width(120)

                TableColumn("Published", value: \.publishedAt) {
                    Text($0.publishedAt)
                }.width(120)

                TableColumn("Duration") {
                    Text($0.duration)
                }.width(50)
            }
        }.searchable(text: $searchTerm, prompt: "Video name") // <-- HERE
    }

}

Upvotes: 3

Related Questions