How to throttle (based on input speed) in iOS UISearchBar?

I have a UISearchBar part of the UISearchDisplayController that is used to display the search results of both the local CoreData and the remote API. What I want to achieve is to “delay” the search in the remote API. Currently, a request is sent for each character entered by the user. But if the user types especially fast, it makes no sense to send a lot of requests: this will help to wait until he stops typing. Is there any way to achieve this?

Reading the documentation suggests waiting for users to explicitly click on a search, but I don't find it ideal in my case.

Performance issues. If the search operations can be performed very quickly, you can update the search results, as the user input by implementing the searchBar: textDidChange: method to delegate the object. However, if the search operation takes longer, you should wait for the user to remove the Search button before searching in the searchBarSearchButtonClicked: method. Always perform background thread operations to avoid blocking the main thread. This allows your application to respond to the user during the search. and provides a better user interface.

Sending many requests to the API is not a local performance issue, but only to prevent the request speed on the remote server from being too high.

thank

+46
ios search
Jun 20 '14 at 14:50
source share
8 answers

Thanks to this link , I found a very quick and clean approach. Compared to Nirmit, there is no “load indicator” response to it, however, it wins in terms of the number of lines of code and does not require additional controls. First I added the dispatch_cancelable_block.h file to my project (from this repo ), and then I defined the following class variable: __block dispatch_cancelable_block_t searchBlock; .

My search code is as follows:

 - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { if (searchBlock != nil) { //We cancel the currently scheduled block cancel_block(searchBlock); } searchBlock = dispatch_after_delay(searchBlockDelay, ^{ //We "enqueue" this block with a certain delay. It will be canceled if the user types faster than the delay, otherwise it will be executed after the specified delay [self loadPlacesAutocompleteForInput:searchText]; }); } 

Notes:

  • loadPlacesAutocompleteForInput is part of the LPGoogleFunctions library
  • searchBlockDelay defined as follows outside of @implementation :

    static CGFloat searchBlockDelay = 0.2;

+13
Jul 05 '14 at 15:24
source share
— -

Try this magic:

 -(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText{ // to limit network activity, reload half a second after last key press. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(reload) object:nil]; [self performSelector:@selector(reload) withObject:nil afterDelay:0.5]; } 

Quick version:

  func searchBar(searchBar: UISearchBar, textDidChange searchText: String) { // to limit network activity, reload half a second after last key press. NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "reload", object: nil) self.performSelector("reload", withObject: nil, afterDelay: 0.5) } 

Please note that this example calls a method called reload, but you can force it to call any method you like!

+83
Apr 21 '15 at 0:20
source share

For people who need it in Swift

 func searchBar(searchBar: UISearchBar, textDidChange searchText: String) { // to limit network activity, reload half a second after last key press. NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: "reload", object: nil) self.performSelector("reload", withObject: nil, afterDelay: 0.5) } 

EDIT: SWIFT 3 Version

 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") } 
+31
Dec 31 '15 at 10:11
source share

A quick hack will look like this:

 - (void)textViewDidChange:(UITextView *)textView { static NSTimer *timer; [timer invalidate]; timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(requestNewDataFromServer) userInfo:nil repeats:NO]; } 

Each time the text representation changes, the timer becomes invalid, as a result of which it does not start. A new timer is created, which starts after 1 second. The search is updated only after the user stops typing for 1 second.

+7
Jul 05 '14 at 15:39
source share

See the following code I found in cocoa controls. They send the request asynchronously to retrieve the data. Maybe they get data from the local one, but you can try it using the remote API. Send an async request to the remote API in the background thread. Follow the link below:

https://www.cocoacontrols.com/controls/jcautocompletingsearch

+2
Jun 26 '14 at 12:48
source share

We can use dispatch_source

 + (void)runBlock:(void (^)())block withIdentifier:(NSString *)identifier throttle:(CFTimeInterval)bufferTime { if (block == NULL || identifier == nil) { NSAssert(NO, @"Block or identifier must not be nil"); } dispatch_source_t source = self.mappingsDictionary[identifier]; if (source != nil) { dispatch_source_cancel(source); } source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(source, dispatch_time(DISPATCH_TIME_NOW, bufferTime * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0); dispatch_source_set_event_handler(source, ^{ block(); dispatch_source_cancel(source); [self.mappingsDictionary removeObjectForKey:identifier]; }); dispatch_resume(source); self.mappingsDictionary[identifier] = source; } 

Read more about Throttling Block Execution Using GCD

If you are using ReactiveCocoa , consider the throttle method on RACSignal

Here you have the ThrottleHandler in Swift .

+2
Jul 15 '15 at 16:36
source share

Swift 2.0 version of NSTimer solution:

 private var searchTimer: NSTimer? func doMyFilter() { //perform filter here } func searchBar(searchBar: UISearchBar, textDidChange searchText: String) { if let searchTimer = searchTimer { searchTimer.invalidate() } searchTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(MySearchViewController.doMyFilter), userInfo: nil, repeats: false) } 
+2
Aug 17 '16 at 3:42 on
source share

Improved Swift 4:

Assuming you already match UISearchBarDelegate , this is an improved version of Swift 4 VivienG's answer :

 func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.reload(_:)), object: searchBar) perform(#selector(self.reload(_:)), with: searchBar, afterDelay: 0.75) } @objc func reload(_ searchBar: UISearchBar) { guard let query = searchBar.text, query.trimmingCharacters(in: .whitespaces) != "" else { print("nothing to search") return } print(query) } 

The goal of the implementation of cancelPreviousPerformRequests (withTarget :) is to prevent the continuous reload() call for each change in the search string (without adding it if you typed "abc", reload() will be called three times depending on the number of characters added).

improvement : the reload() method has a sender parameter, which is a search string; Thus, access to its text or any of its methods / properties would be available with the declaration of it as a global property in the class.

+1
Dec 22 '17 at 17:35
source share



All Articles