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

80

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

maggix
fuente
1
No estoy seguro de que el título sea correcto. Lo que está pidiendo se llama "antirrebote", no "acelerador".
V_tredue

Respuestas:

132

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.

malhal
fuente
esto funciona muy bien ... ¡no sabía sobre el método cancelPreviousPerformRequestsWithTarget!
jesses.co.tt
¡De nada! Es un gran patrón y se puede utilizar para todo tipo de cosas.
Malhal
¡Muy útil! Este es el verdadero vudú
Matteo Pacini
2
En cuanto a "recargar" ... tuve que pensar en ello por un par de segundos extra ... Eso se refiere al método local que realmente realizará las cosas que desea hacer después de que el usuario deje de escribir durante 0,5 segundos. El método podría llamarse como desee, como searchExecute. ¡Gracias!
blalond
esto no funciona para mí ... continúa ejecutando la función "recargar" cada vez que se cambia una letra
Andrey
52

Para las personas que necesitan esto en Swift 4 en adelante :

Mantenlo simple con un me DispatchWorkItemgusta 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")
}
VivienG
fuente
1
¡Buena respuesta! Acabo de agregarle una pequeña mejora, puedes verlo :)
Ahmad F
Gracias @AhmadF, estaba pensando en hacer una actualización de SWIFT 4. ¡Lo hiciste! : D
VivienG
1
Para Swift 4, utilice DispatchWorkItemcomo se sugirió anteriormente. Funciona con elegancia que los selectores.
Teffi
21

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.

Ahmad F
fuente
Es realmente útil para mí, analizar con el objeto de la barra de búsqueda en el selector
Hari Narayanan
Acabo de intentarlo en OBJC - (void) searchBar: (UISearchBar *) searchBar textDidChange: (NSString *) searchText {[NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector (validateText :) object: searchBar]; [self performSelector: @selector (validateText :) withObject: searchBar afterDelay: 0.5]; }
Hari Narayanan
18

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.harchivo 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:

  • El loadPlacesAutocompleteForInputes parte de la LPGoogleFunctions biblioteca
  • searchBlockDelayse define de la siguiente manera fuera de @implementation:

    CGFloat estático searchBlockDelay = 0.2;

maggix
fuente
1
El enlace a la publicación del blog me parece muerto
jeroen
1
@jeroen tienes razón: lamentablemente parece que el autor eliminó el blog de su sitio web. El repositorio en GitHub que hacía referencia a ese blog aún está activo, por lo que es posible que desee verificar el código aquí: github.com/SebastienThiebaud/dispatch_cancelable_block
maggix
el código dentro del searchBlock nunca se ejecuta. ¿Es necesario más código?
comienza
12

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.

duci9y
fuente
Parece que tuvimos el mismo enfoque, y este ni siquiera requiere código adicional. Aunque el requestNewDataFromServermétodo debe modificarse para obtener el parámetro deluserInfo
maggix
Sí, modifícalo según tus necesidades. El concepto es el mismo.
duci9y
3
dado que el temporizador nunca se dispara en este enfoque, descubrí que falta una línea aquí: [[NSRunLoop mainRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode];
comienza
@itinance ¿A qué te refieres? El temporizador ya está en el ciclo de ejecución actual cuando lo crea con el método en el código.
duci9y
Esta es una solución rápida y ordenada. También puede usar esto en sus otras solicitudes de red, como en mi situación, obtengo nuevos datos cada vez que el usuario arrastra su mapa. Solo tenga en cuenta que en Swift, querrá crear una instancia de su objeto de temporizador llamando al scheduledTimer....
Glenn Posadas
5

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

}
GSnyder
fuente
3

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

Nirmit Dagly
fuente
¡Hola! Finalmente tuve tiempo de echar un vistazo a tu control sugerido. Definitivamente es interesante y no tengo ninguna duda de que muchos se beneficiarán de él. Sin embargo, creo que encontré una solución más corta (y, en mi opinión, más limpia) de esta publicación de blog, gracias a un poco de inspiración de su enlace: sebastienthiebaud.us/blog/ios/gcd/block/2014/04/09/…
maggix
@maggix, el enlace que ha proporcionado ha caducado ahora. ¿Puede sugerir algún otro enlace?
Nirmit Dagly
Estoy actualizando todos los enlaces de este hilo. Use el de mi respuesta a continuación ( github.com/SebastienThiebaud/dispatch_cancelable_block )
maggix
También mire esto, si está utilizando Google Maps. Esto es compatible con iOS 8 y está escrito en aim-c. github.com/hkellaway/HNKGooglePlacesAutocomplete
Nirmit Dagly
3

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 throttlemétodo enRACSignal

Aquí está ThrottleHandler en Swift si está interesado

onmyway133
fuente
3

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)
}
William T.
fuente