Tengo una parte UISearchBar de un UISearchDisplayController que se usa para mostrar resultados de búsqueda tanto de CoreData local como de API remota. Lo que quiero lograr es el "retraso" de la búsqueda en la API remota. Actualmente, por cada carácter escrito por el usuario, se envía una solicitud. Pero si el usuario escribe particularmente rápido, no tiene sentido enviar muchas solicitudes: sería útil esperar hasta que deje de escribir. ¿Hay alguna forma de lograrlo?
Leer la documentación sugiere esperar hasta que los usuarios toquen explícitamente la búsqueda, pero no lo encuentro ideal en mi caso.
Problemas de desempeño. Si las operaciones de búsqueda se pueden llevar a cabo muy rápidamente, es posible actualizar los resultados de la búsqueda mientras el usuario escribe mediante la implementación del 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 :. Realice siempre las operaciones de búsqueda en un hilo en segundo plano para evitar bloquear el hilo principal. Esto hace que su aplicación responda al usuario mientras se ejecuta la búsqueda y brinda una mejor experiencia de usuario.
Enviar muchas solicitudes a la API no es un problema de rendimiento local, sino solo de evitar una tasa de solicitud demasiado alta en el servidor remoto.
Gracias
fuente
Respuestas:
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 rápida:
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.
fuente
Para las personas que necesitan esto en Swift 4 en adelante :
Mantenlo simple con un me
DispatchWorkItem
gusta aquí .o use 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: SWIFT 3 Versión
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") }
fuente
DispatchWorkItem
como se sugirió anteriormente. Funciona con elegancia que los selectores.Swift 4 mejorado:
Suponiendo que ya está conforme
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 la llamada continua al
reload()
para cada cambio en la barra de búsqueda (sin agregarlo, si escribió "abc",reload()
se llamará tres veces según el número de caracteres agregados) .La mejora es: en el
reload()
método tiene el parámetro del remitente que es la barra de búsqueda; Por lo tanto, acceder a su texto -o cualquiera de sus métodos / propiedades- sería accesible declarándolo como una propiedad global en la clase.fuente
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. He añadido en primer lugar el
dispatch_cancelable_block.h
archivo a mi proyecto (de este repo ), entonces se define 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 LPGoogleFunctions bibliotecasearchBlockDelay
se define de la siguiente manera fuera de@implementation
:CGFloat estático searchBlockDelay = 0.2;
fuente
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 de texto, el temporizador se invalida, lo que hace que no se active. Se crea un nuevo temporizador y se activa después de 1 segundo. La búsqueda solo se actualiza después de que el usuario deja de escribir durante 1 segundo.
fuente
requestNewDataFromServer
método debe modificarse para obtener el parámetro deluserInfo
scheduledTimer...
.Solución Swift 4, más algunos comentarios generales:
Todos estos son enfoques razonables, pero si desea un comportamiento de búsqueda automática ejemplar, realmente necesita dos temporizadores o despachos separados.
El comportamiento ideal es que 1) la búsqueda automática se active periódicamente, pero 2) no con demasiada frecuencia (debido a la carga del servidor, el ancho de banda celular y la posibilidad de provocar 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 largo plazo que se activa tan pronto como comienza la edición (sugiero 2 segundos) y se le permite ejecutarse independientemente de la actividad posterior, más un temporizador a corto plazo (~ 0,75 segundos) que se reinicia en cada cambio. La expiración de cualquiera de los temporizadores activa la búsqueda automática y restablece ambos temporizadores.
El efecto neto es que la escritura continua produce búsquedas automáticas cada segundos de período largo, pero se garantiza que una pausa desencadenará una búsqueda automática en segundos.
Puede implementar este comportamiento de manera muy simple 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... }
El 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 usa temporizadores, pero puede modificarla en términos de operaciones de envío 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() } }
fuente
Consulte el siguiente código que encontré en los controles de cacao. Están enviando solicitudes de forma asincrónica para obtener los datos. Puede que estén obteniendo datos de forma local, pero puede probarlo con la API remota. Envíe una solicitud asíncrona en una API remota en un hilo de fondo. Siga el enlace a continuación:
https://www.cocoacontrols.com/controls/jcautocompletingsearch
fuente
Nosotros 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 la limitación de la ejecución de un bloque con GCD
Si está usando ReactiveCocoa , considere el
throttle
método enRACSignal
Aquí está ThrottleHandler en Swift si está interesado
fuente
Versión Swift 2.0 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) }
fuente