I need to filter my model data with a search bar. I added the .searchable()
property and when the search text changes I filter my objects with fuzzy matching. This takes too much time and the app lags when writing into the search box. So I want to do the searching asynchronously so that the app doesn't freeze.
I tried to do it with the onChange(of:)
property and then I create a Task
that runs the async function because the onChange()
property doesn't allow async functions by themselves. But the app still lags.
Here is a code example of how I tried doing it:
import SwiftUI
import Fuse
struct SearchView: View {
@EnvironmentObject var modelData: ModelData
@State var searchText = ""
@State var searchResults: [Item] = []
@State var searchTask: Task<(), Never>? = nil
let fuseSearch = Fuse()
var body: some View {
// Show search results
}
.searchable(text: $searchText)
.onChange(of: searchText) { newQuery in
// Cancel if still searching
searchTask?.cancel()
searchTask = Task {
searchResults = await fuzzyMatch(items: modelData.items, searchText: newQuery)
}
}
func fuzzyMatch(items: [Item], searchText: String) async -> [Item] {
filteredItems = items.filter {
(fuseSearch.search(searchText, in: $0.name)?.score ?? 1) < 0.25
}
return filteredItems
}
}
I would really appreciate some help.
CodePudding user response:
I think the main problem is debouncing as lorem ipsum mentioned before. I just tested my code and you need to call your filter method where i printed.
In this way you will not filter for every editing textfield. You will filter after some millisecond which you may change.
You can find more detail in this link SwiftUI Combine Debounce TextField
struct Example: View {
@State var searchText = ""
let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
NavigationView {
Text("Test")
}
.searchable(text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
print("call your filter method")
}
}
}
CodePudding user response:
There are a few issues:
The
fuzzyMatch
is marked asasync
, but is not currently doing anything asynchronous. Having the method signature reflect what is going on may make it easier to reason about one’s code.To that end, now that we realize that
fuzzyMatch
runs synchronously, it becomes apparent that if it runs slowly, it will block the current thread. And becauseTask { ... }
runs on the current actor, you will end up blocking the main thread. You should consider usingTask.detached
to get it on a background thread. But marksearchResults
as being on the main actor.If you cancel the
Task
, it will not stopfuzzyMatch
that is underway. It should check for cancelation.
So, pulling that all together, perhaps:
struct ContentView: View {
@StateObject var modelData: ModelData()
@State var searchText = ""
@MainActor @State var searchResults: [Item] = []
@State var searchTask: Task<[Item], Error>?
// let fuseSearch = Fuse()
var body: some View {
NavigationStack {
...
}
.searchable(text: $searchText)
.onChange(of: searchText) { newQuery in
Task {
searchTask?.cancel()
let task = Task.detached {
try await Task.sleep(seconds: 0.5) // debounce; if you don't want debouncing, remove this, but it can eliminate annoying updates of the UI while the user is typing
return try await fuzzyMatch(items: modelData.items, searchText: newQuery)
}
searchTask = task
searchResults = try await task.value
}
}
}
func fuzzyMatch(items: [Item], searchText: String) throws -> [Item] {
try items.filter {
try Task.checkCancellation()
return (fuseSearch.search(searchText, in: $0.name)?.score ?? 1) < 0.25
}
}
}
It is not terribly relevant, but the above uses these extensions:
extension Task where Success == Never, Failure == Never {
public static func sleep(seconds: TimeInterval) async throws {
let nanoseconds = seconds * .nanosecondsPerSecond
try await sleep(nanoseconds: UInt64(nanoseconds))
}
}
extension TimeInterval {
static let nanosecondsPerSecond = Self(NSEC_PER_SEC)
}