Live search throttle in Swift 3
Asked Answered
A

3

13

I'm trying to live search at my PHP API with Swift. Until now i've done this thing.

 var filteredData = [Products]()


 func getSearch(completed: @escaping DownloadComplete, searchString: String) {

        let parameters: Parameters = [
            "action" : "search",
            "subaction" : "get",
            "product_name"  : searchString,
            "limit" : "0,30"
        ]
        Alamofire.request(baseurl, method: .get, parameters: parameters).responseJSON { (responseData) -> Void in
            if((responseData.result.value) != nil) {
                let result = responseData.result

                if let dict = result.value as? Dictionary<String, AnyObject>{
                    if let list = dict["products_in_category"] as? [Dictionary<String, AnyObject>] {
                        if self.filteredData.isEmpty == false {
                            self.filteredData.removeAll()
                        }
                        for obj in list {
                            let manPerfumes = Products(productDict: obj)
                            self.filteredData.append(manPerfumes)
                        }
                    }
                }
                completed()
            }
        }
    }


extension SearchViewController: UISearchResultsUpdating {

    func updateSearchResults(for searchController: UISearchController) {

        if (searchController.searchBar.text?.characters.count)! >= 3 {
                    self.getSearch(completed: {
                    self.searchResultTable.reloadData()

                    self.searchResultTable.setContentOffset(CGPoint.zero, animated: true)
                }, searchString: searchController.searchBar.text!)


        } else {
            self.searchResultTable.reloadData()
        }


    }
}

And the table view is being updated with the filteredData. How can i throttle the search so lets say when the user writes

"example" -> shows the results with example
then he erase the "le" -> 
"examp" -> if the previous request is not completed, cancel it -> make request for "examp" and show the data in table view!

P.S. from another answer i found

func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
    // to limit network activity, reload half a second after last key press.
    NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.reload), object: nil)
    self.perform(#selector(self.reload), with: nil, afterDelay: 0.5)
}
func reload() {
    print("Doing things")
}

Although if I try to replace "self.reload" with my function, I get an error cannot convert value of type () to expected argument type selector

Association answered 24/2, 2017 at 17:2 Comment(6)
how did you replace the self.reload function into the cancelPreviousPerformRequests selector parameter? It seems like you forgot to put that function in #selector?Gormandize
Because if I try doing it like this, I get the same error as you did: NSObject.cancelPreviousPerformRequests(withTarget: self, selector: self.fetchFriends(user:completion:), object: nil). Notice how I didn't add the #selector into my function?Gormandize
@rockyraccoon still the same error!Association
well, I tried replicating what you're trying to accomplish and I get no errors: func searchBar() { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.getSearch(completed:searchString:)), object: nil) perform(#selector(self.getSearch(completed:searchString:)), with: nil, afterDelay: 0.5) } Gormandize
@rockyraccoon ok this is error free! you where right. Although in perform, how can i pass string to the function?Association
that's what worried me lol. But I just found a function for that. Try to use this function: func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!). This is in the docs, or you can auto complete it to see it.Gormandize
G
3

Your error was because you probably forgot the #selector() part.

Here's how it should look:

func searchBar() { 
    NSObject.cancelPreviousPerformRequests(withTarget: self,    
                                           selector: #selector(self.getSearch(completed:searchString:)), 
                                           object: nil) 

    perform(#selector(self.getSearch(completed:searchString:)), 
            with: nil, afterDelay: 0.5) }

You get the error because you didn't enclose your function in #selector

Now, as for the arguments, here's a function for that:

perform(#selector(getSearch:completion:searchString), with: <some completion>, with: "search string")
Gormandize answered 24/2, 2017 at 18:18 Comment(4)
And if i would like to add timeinterval is not possible :(Association
You can do that. BUT, if you notice on autocomplete, there's a function that has timeInterval, except it only has one parameter to put it in your argument. My idea was, you can extend the class to add more arguments, but I feel that's a bit too much. Perhaps you should ask another question pertaining to this certain issue:)Gormandize
your answer is correct for this issue! I open another one!Association
#42446368Association
C
71

Use DispatchWorkItem with Swift 4 !

// Add a searchTask property to your controller
var searchTask: DispatchWorkItem?


// then in your search bar update method

// Cancel previous task if any
self.searchTask?.cancel()

// Replace previous task with a new one
let task = DispatchWorkItem { [weak self] in
    self?.sendSearchRequest() 
}
self.searchTask = task

// Execute task in 0.75 seconds (if not cancelled !)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.75, execute: task)

Hope it helps !

Clew answered 7/2, 2018 at 14:12 Comment(1)
FYI, Cancellation does not affect the execution of a work item that has already begun. according to developer.apple.com/documentation/dispatch/dispatchworkitem/…Malfeasance
G
3

Your error was because you probably forgot the #selector() part.

Here's how it should look:

func searchBar() { 
    NSObject.cancelPreviousPerformRequests(withTarget: self,    
                                           selector: #selector(self.getSearch(completed:searchString:)), 
                                           object: nil) 

    perform(#selector(self.getSearch(completed:searchString:)), 
            with: nil, afterDelay: 0.5) }

You get the error because you didn't enclose your function in #selector

Now, as for the arguments, here's a function for that:

perform(#selector(getSearch:completion:searchString), with: <some completion>, with: "search string")
Gormandize answered 24/2, 2017 at 18:18 Comment(4)
And if i would like to add timeinterval is not possible :(Association
You can do that. BUT, if you notice on autocomplete, there's a function that has timeInterval, except it only has one parameter to put it in your argument. My idea was, you can extend the class to add more arguments, but I feel that's a bit too much. Perhaps you should ask another question pertaining to this certain issue:)Gormandize
your answer is correct for this issue! I open another one!Association
#42446368Association
O
0
  • disclaimer: I am a writer.

Throttler could be the right tool to get it done.

You can do debounce and throttle without going reactive programming using Throttler like,

import Throttler

// advanced debounce, running a first task immediately before initiating debounce.

for i in 1...1000 {
    Throttler.debounce {
        print("debounce! > \(i)")
    }
}

// debounce! > 1
// debounce! > 1000


// equivalent to debounce of Combine, RxSwift.

for i in 1...1000 {
    Throttler.debounce(shouldRunImmediately: false) {
        print("debounce! > \(i)")
    }
}

// debounce! > 1000

Throttler also can do advanced debounce, running a first event immediately before initiating debounce that Combine and RxSwift don't have by default.

You could, but you may need a complex implementation yourself for that.

Outlandish answered 23/3, 2021 at 17:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.