¿Cómo acelerar la búsqueda (según la velocidad de tipeo) en iOS UISearchBar?

Tengo una parte UISearchBar de UISearchDisplayController que se utiliza para mostrar los resultados de búsqueda tanto de CoreData local como de la API remota. Lo que quiero lograr es la “demora” de la búsqueda en la API remota. Actualmente, para cada carácter escrito por el usuario, se envía una solicitud. Pero si el usuario escribe de forma particularmente rápida, no tiene sentido enviar muchas solicitudes: sería útil esperar hasta que haya dejado de escribir. ¿Hay alguna manera de lograr eso?

Leer la documentación sugiere esperar hasta que los usuarios expliquen explícitamente la búsqueda, pero no me parece ideal en mi caso.

Problemas de desempeño. Si las operaciones de búsqueda se pueden llevar a cabo con gran rapidez, es posible actualizar los resultados de búsqueda a medida que el usuario escribe utilizando el método searchBar: textDidChange: en el objeto delegado. Sin embargo, si una operación de búsqueda lleva más tiempo, debe esperar hasta que el usuario toque el botón Buscar antes de comenzar la búsqueda en el método searchBarSearchButtonClicked :. Siempre realice operaciones de búsqueda con una secuencia de fondo para evitar el locking del hilo principal. Esto mantiene su aplicación sensible al usuario mientras se ejecuta la búsqueda y proporciona una mejor experiencia de usuario.

El envío de muchas solicitudes a la API no es un problema de rendimiento local sino que solo evita una tasa de solicitud demasiado alta en el servidor remoto.

Gracias

Prueba esta magia:

-(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]; } 

Versión 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) } 

Tenga en cuenta que este ejemplo llama a un método llamado reload pero puede hacer que llame al método que desee.

Para las personas que necesitan esto en Swift

Mantenlo simple con un DispatchWorkItem como aquí: https://stackoverflow.com/a/48666001/308315

o usa la antigua forma de Obj-C:

 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) } 

EDITAR: Versión SWIFT 3

 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") } 

Gracias a este enlace , encontré un enfoque muy rápido y limpio. En comparación con la respuesta de Nirmit, carece del “indicador de carga”, sin embargo, gana en términos de número de líneas de código y no requiere controles adicionales. Primero agregué el archivo dispatch_cancelable_block.h a mi proyecto (desde este repository ), luego __block dispatch_cancelable_block_t searchBlock; la siguiente variable de clase: __block dispatch_cancelable_block_t searchBlock; .

Mi código de búsqueda ahora se ve así:

 - (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]; }); } 

Notas:

  • loadPlacesAutocompleteForInput es parte de la biblioteca LPGoogleFunctions
  • searchBlockDelay se define de la siguiente manera fuera de @implementation :

    static CGFloat searchBlockDelay = 0.2;

Un truco rápido sería así:

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

Cada vez que cambia la vista del texto, el temporizador se invalida y hace que no se dispare. Se crea un nuevo temporizador y se configura para disparar después de 1 segundo. La búsqueda solo se actualiza después de que el usuario deja de escribir durante 1 segundo.

Mejorado Swift 4:

Asumiendo que ya se está conformando con UISearchBarDelegate , esta es una versión mejorada de Swift 4 de la respuesta de VivienG :

 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) } 

El propósito de implementar cancelPreviousPerformRequests (withTarget 🙂 es evitar las llamadas continuas a reload() para cada cambio en la barra de búsqueda (sin agregarlo, si escribió “abc”, reload() se invocará tres veces según el número de los caracteres agregados).

La mejora es: en el método reload() tiene el parámetro del remitente que es la barra de búsqueda; De este modo, se puede acceder a su texto, o cualquiera de sus métodos / propiedades, y declararlo como una propiedad global en la clase.

Swift 2.0 versión de la solución NSTimer:

 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) } 

Por favor, mira el siguiente código que he encontrado en los controles de cocoa. Están enviando solicitudes de forma asíncrona para recuperar los datos. Puede ser que estén recibiendo datos de locales, pero puedes probarlos con la API remota. Enviar solicitud asíncrona en la API remota en el hilo de fondo. Siga el enlace a continuación:

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

Podemos usar 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; } 

Más sobre Estrangular una ejecución en bloque usando GCD

Si está utilizando ReactiveCocoa , considere throttle método de throttle en RACSignal

Aquí está ThrottleHandler en Swift en el que está interesado

Solución Swift 4, más algunos comentarios generales:

Todos estos son enfoques razonables, pero si desea un comportamiento de autosearch ejemplar, realmente necesita dos temporizadores o despachos por separado.

El comportamiento ideal es que 1) autosearch se active periódicamente, pero 2) no con demasiada frecuencia (debido a la carga del servidor, el ancho de banda celular y el potencial de causar tartamudeos en la interfaz de usuario), y 3) se active rápidamente tan pronto como haya una pausa en la escritura del usuario.

Puede lograr este comportamiento con un temporizador a más largo plazo que se activa tan pronto como comienza la edición (sugiero 2 segundos) y puede ejecutarse independientemente de la actividad posterior, más un temporizador a corto plazo (~ 0.75 segundos) que se restablece en cada cambio. La expiración de cualquiera de los temporizadores activa autosearch y restablece ambos temporizadores.

El efecto neto es que el tipado continuo genera autosegundos cada segundo largo de segundos, pero se garantiza que una pausa activará un autosearch dentro de cortos periodos de segundos.

Puede implementar este comportamiento muy simplemente con la clase AutosearchTimer a continuación. He aquí cómo usarlo:

 // The closure specifies how to actually do the autosearch lazy var timer = AutosearchTimer { [weak self] in self?.performSearch() } // Just call activate() after all user activity func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { timer.activate() } func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { performSearch() } func performSearch() { timer.cancel() // Actual search procedure goes here... } 

AutosearchTimer maneja su propia limpieza cuando se libera, por lo que no hay necesidad de preocuparse por eso en su propio código. Pero no le dé al temporizador una fuerte referencia a sí mismo o creará un ciclo de referencia.

La implementación a continuación utiliza temporizadores, pero puede modificarla en términos de operaciones de despacho si lo prefiere.

 // Manage two timers to implement a standard autosearch in the background. // Firing happens after the short interval if there are no further activations. // If there is an ongoing stream of activations, firing happens at least // every long interval. class AutosearchTimer { let shortInterval: TimeInterval let longInterval: TimeInterval let callback: () -> Void var shortTimer: Timer? var longTimer: Timer? enum Const { // Auto-search at least this frequently while typing static let longAutosearchDelay: TimeInterval = 2.0 // Trigger automatically after a pause of this length static let shortAutosearchDelay: TimeInterval = 0.75 } init(short: TimeInterval = Const.shortAutosearchDelay, long: TimeInterval = Const.longAutosearchDelay, callback: @escaping () -> Void) { shortInterval = short longInterval = long self.callback = callback } func activate() { shortTimer?.invalidate() shortTimer = Timer.scheduledTimer(withTimeInterval: shortInterval, repeats: false) { [weak self] _ in self?.fire() } if longTimer == nil { longTimer = Timer.scheduledTimer(withTimeInterval: longInterval, repeats: false) { [weak self] _ in self?.fire() } } } func cancel() { shortTimer?.invalidate() longTimer?.invalidate() shortTimer = nil; longTimer = nil } private func fire() { cancel() callback() } }