Reputation: 3336
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
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
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