Reputation: 591
I know there have been I would say similar questions, but none of them fits my case -> see my already tried attempts below.
So what I'm trying to do is make a advanced searchBar. Therefore, everytime the searchParam changed, I need to execute code to check whether my TableViewItems (Array) match the criteria -> filter.
When smo. for example types 3 characters, my code checks this string. But this will take a while to get all results.
The Problem: When smo. then types the 4th character, I want to stop the previous execution and start a new one with the string of 4.
My Attempts:
Using DispatchWorkItem:
The problem here is that it only changes a Boolean and it take 10sec+ for the code to recognize that it changed. Works if I execute it .sync
instead of .async
, but it freezes the app for more than 10sec
Using DispatchQueue:
Can't be stopped, only paused -> so will remain in memory -> will spam memory
Checking Boolean in every for loop
:
Same as with DispatchWorkItem, will take 10+ sec to recognize a change
Current Code: (not very important)
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
self.stop = true
if people != nil {
if searchText.isEmpty {
searchedPeople = people
self.stop = false
} else {
self.searchedPeople = []
self.tableView.reloadData()
workItem = DispatchWorkItem(qos: .userInitiated, flags: []) {
outter: for i in 0...searchText.count - 1 {
if self.stop { self.stop = false;break outter } //
if i == 0 {
inner: for person in self.people! {
if self.stop { self.stop = false;break outter } //
let name = (person.Vorname != nil ? person.Vorname! + " " : "") + (person.Nachname ?? "")
if name.lowercased().contains(searchText.lowercased()) {
print("Found: \(name), \(searchText), Exact Match")
self.searchedPeople?.append(person);DispatchQueue.main.async { self.tableView.reloadData()}; continue inner
}
}
} else {
let firstP = searchText.prefix(i+1)
let lastP = searchText.suffix(searchText.count - i + 1)
inner2: for person in self.people! {
if self.stop { self.stop = false;break outter } //
let name = (person.Vorname != nil ? person.Vorname! + " " : "") + (person.Nachname ?? "")
if name.lowercased().contains(firstP.lowercased()) && (name.lowercased().contains(lastP.lowercased())) {
print("Found: \(name), \(searchText), First: \(firstP), Last: \(lastP)")
self.searchedPeople?.append(person)
DispatchQueue.main.async { self.tableView.reloadData()}
continue inner2
}
}
}
}
}
//execute
DispatchQueue.async(execute: workItem)
}
}
}
Upvotes: 1
Views: 1460
Reputation: 114975
I am not sure that cancelling the operation is your real issue. Your search code is pretty inefficient. Refactoring so that the outer loop is your people array rather than your search string will make the operation much faster.
You can also compute a lot of values once.
Using the code below I was able to find 1438 matches from 24636 candidates in 0.32 seconds (or 0.23 seconds if I commented out the print "Found:...").
func search(_ people: [Person], for searchText: String) {
guard !people.isEmpty else {
print("No people")
return
}
let lcSearch = searchText.lowercased()
print("Starting search for \(searchText)")
var searchedPeople = [Person]()
for person in people {
let personName = ((person.firstName ?? "") + " " + (person.surname ?? "")).lowercased()
if personName.contains(lcSearch) {
searchedPeople.append(person)
continue
}
for i in 1..<lcSearch.count {
let split = lcSearch.index(lcSearch.startIndex, offsetBy: i)
let firstP = lcSearch.prefix(upTo: split)
let lastP = lcSearch.suffix(from: split)
if personName.contains(firstP) && personName.contains(lastP) {
print("Found: \(personName), \(searchText), First: \(firstP), Last: \(lastP)")
searchedPeople.append(person)
break
}
}
}
print("Done. Found \(searchedPeople.count)")
}
Upvotes: 0
Reputation: 17060
Your problem is, essentially, that you're checking global state when you should be checking local state. Let's say you've got operation 'L' going on, and you've just typed an 'e', so operation 'Le' is going to start. Here's what roughly happens:
func updateMyPeepz() {
// At this point, `self.workItem` contains operation 'L'
self.workItem.cancel() // just set isCancelled to true on operation 'L'
self.workItem = DispatchWorkItem { /* bla bla bla */ }
// *now* self.workItem points to operation 'Le'!
}
So later, in the work item for operation 'L', you do this:
if self.workItem.isCancelled { /* do something */ }
Unfortunately, this code is running in operation 'L', but self.workItem
now points to operation 'Le'! So while operation 'L' is cancelled, and operation 'Le' is not, operation 'L' sees that self.workItem
—i.e. operation 'Le'—is not cancelled. And thus, the check always returns false
, and operation 'L' never actually stops.
When you use the global boolean variable, you have the same problem, because it's global and doesn't differentiate between the operations that should still be running and the ones that shouldn't (in addition to atomicity issues you're already going to introduce if you don't protect the variable with a semaphore).
Here's how to fix it:
func updateMyPeepz() {
var workItem: DispatchWorkItem? = nil // This is a local variable
self.currentWorkItem?.cancel() // Cancel the one that was already running
workItem = DispatchWorkItem { /* bla */ } // Set the *local variable* not the property
self.currentWorkItem = workItem // now we set the property
DispatchQueue.global(qos: whatever).async(execute: workItem) // run it
}
Now, here's the crucial part. Inside your loops, check like this:
if workItem?.isCancelled ?? false // check local workItem, *not* self.workItem!
// (you can also do workItem!.isCancelled if you want, since we can be sure this is non-nil)
At the end of the workItem, set workItem to nil
to get rid of the retain cycle (otherwise it'll leak):
workItem = nil // not self.workItem! Nil out *our* item, not the property
Alternatively, you can put [weak workItem] in
at the top of the workItem
block to prevent the cycle—then you don't have to nil it out at the end, but you should be sure to use ?? false
instead of !
since you always want to assume that a weak variable can conceivably go nil at any time.
Upvotes: 3
Reputation: 5804
Edit: The 10 second delay is most likely due to some issue updating the UI. I just tested updating a shared boolean between the main queue and a dispatch work item on a background queue, and there was no apparent propagation delay of that magnitude. However, there's debate over whether this is safe here and here.
The self.tableView.reloadData()
happening in the async blocks of the work item and also in the sync part before the work item... I'm not sure what behavior that'll create since you're relying on the order of the reloadData
calls in the GCD queues behind the scenes. It'd be more predictable if your work items just built a local array of results and didn't semi-directly interact with the UI.
One proposal, not necessarily the best cause it's been a while since I've used Dispatch: Have your work items find the array of results then update a shared [String: [Person]]
dict mapping search strings to result arrays (lock needed? not sure) when they're done. Then you could use DispatchWorkItem.notify
(example) to run code on the main queue that updates the UI table when a work item finishes, using the shared dictionary result that matches the current typed search string or doing nothing if no result matches.
P.S. This is a lot of manual work, and I might be missing an easier way. I know CoreData automates the task of searching and updating a UI table with its NSFetchedResultsController
, but that's a whole different setup.
Upvotes: 1