I have a search method for a search bar component in my Swift app. Here is the search method:
@MainActor
func searchLocalPopularTerms(for searchTerm: String, completionHandler: @escaping (() -> Void)) {
// If timestamp is nil, this is our first search, so set it here
if self.timestamp == nil {
self.timestamp = Date()
}
guard let timestamp else { return }
Task {
// When we make a request we pass a timestamp into the request
let searchResults = await self.searchManager.searchResults(searchTerm: searchTerm, gender: self.gender, timestamp: Date())
// We then check if the timestamp returned by the results is great than the stored timestamp.
// If it is, then these are new results and we can update the searchItems array
if searchResults.results.isNotEmpty, searchResults.timestamp > timestamp {
self.searchItems = searchResults.results
// And then reset the local timestamp to the new current date
self.timestamp = Date()
}
completionHandler()
}
}
Because the SearchManager searchResults()
method is with async await
I am trying to make sure that there is no issue with searches i.e. that the searches are done in the correct order and that there are no threading issues. As such I have tried to add a check here whereby I pass a timestamp into the request which then is set against the results when they are returned from the API. When a successful call is made and results returned, I set the local timestamp
variable. Every time I receive an API response, I am checking the local variable (which will be storing the last successful result timestamp) against the new results. If the new results timestamp is greater than the stored one, I know that this request was indeed made after the most successful one, and so only then do I update the results array.
However, there seems to be something wrong with the implementation as when I type into the search field, sometimes it is clearly not updating with the latest search. It works most of the time, but on occasions if I type very fast, if I have typed, for example, "Sandwiches" if fetches "Sandwi", omitting the last characters. I think there must be something wrong with the way I am implementing this.
Here is an example of where I call this method:
viewModel?.searchLocalPopularTerms(for: searchText, completionHandler: { [weak self] in
onMain {
self?.reloadSection(showKeyboard: true)
self?.viewModel?.searchTerm = searchText
}
})
And the service method itself:
private static func searchResults(_ query: String, gender: Gender, popularTerms: [String]? = nil,
allowFredhopperSuggestions: Bool = ConfigManager.shared.allowFredhopperSuggestions, timestamp: Date) async -> SearchResults {
let suggestionLimits = SearchSuggestionLimits()
var suggestions: [SearchItem] = []
suggestions.reserveCapacity(suggestionLimits.totalLimit)
if allowFredhopperSuggestions {
let results = await fredhopperSuggestions(query)
suggestions.append(contentsOf: results)
} else {
let designers = designerSearchResults(query, gender: gender, fetchLimit: suggestionLimits.designerLimit)
suggestions.append(contentsOf: designers)
let categories = categorySearchResults(query, gender: gender, fetchLimit: suggestionLimits.categoryLimit)
suggestions.append(contentsOf: categories)
}
if let popularTerms = popularTerms {
let alreadyFoundList = suggestions.compactMap { $0.name }
let popularSearches = popularTermsSearchResults(query, popularTerms: popularTerms, alreadyFoundList: alreadyFoundList, fetchLimit: suggestionLimits.popularLimit)
suggestions.append(contentsOf: popularSearches)
}
return SearchResults(timestamp: timestamp, results: suggestions)
}
I've refactored now so that the search method in the viewModel is as follows:
@MainActor
func searchLocalPopularTerms(for searchTerm: String) async {
let searchResults = await self.searchManager.searchResults(searchTerm: searchTerm, gender: self.gender, timestamp: Date())
if searchResults.results.isNotEmpty {
self.searchItems = searchResults.results
}
}
And in the viewController we use the task:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.searchTask?.cancel()
let task = Task { [weak self] in
guard let self else { return }
await self.viewModel?.searchLocalPopularTerms(for: searchText)
self.reloadSection(showKeyboard: true)
self.viewModel?.searchTerm = searchText
}
self.searchTask = task
}
For each new search, we cancel the previous task