web-dev-qa-db-fra.com

Comment limiter la recherche (basée sur la vitesse de frappe) dans iOS UISearchBar?

J'ai une partie UISearchBar d'un UISearchDisplayController qui est utilisé pour afficher les résultats de la recherche à partir de CoreData local et de l'API distante. Ce que je veux réaliser, c'est le "retard" de la recherche sur l'API distante. Actuellement, pour chaque caractère tapé par l'utilisateur, une demande est envoyée. Mais si l'utilisateur tape particulièrement rapidement, cela n'a pas de sens d'envoyer de nombreuses requêtes: il serait utile d'attendre qu'il ait arrêté de taper. Y a-t-il un moyen d'y parvenir?

La lecture de documentation suggère d'attendre que les utilisateurs tapent explicitement sur la recherche, mais je ne trouve pas cela idéal dans mon cas.

Les problèmes de performance. Si les opérations de recherche peuvent être effectuées très rapidement, il est possible de mettre à jour les résultats de la recherche lors de la frappe de l'utilisateur en implémentant la méthode searchBar: textDidChange: sur l'objet délégué. Cependant, si une opération de recherche prend plus de temps, vous devez attendre que l'utilisateur appuie sur le bouton Rechercher avant de commencer la recherche dans la méthode searchBarSearchButtonClicked :. Effectuez toujours des opérations de recherche sur un thread d'arrière-plan pour éviter de bloquer le thread principal. Cela maintient votre application sensible à l'utilisateur pendant la recherche et offre une meilleure expérience utilisateur.

L'envoi de nombreuses requêtes à l'API n'est pas un problème de performances locales mais seulement d'éviter un taux de requêtes trop élevé sur le serveur distant.

Merci

64
maggix

Grâce à ce lien , j'ai trouvé une approche très rapide et propre. Par rapport à la réponse de Nirmit, il lui manque "l'indicateur de chargement", mais il gagne en termes de nombre de lignes de code et ne nécessite pas de contrôles supplémentaires. J'ai d'abord ajouté le dispatch_cancelable_block.h fichier vers mon projet (de ce dépôt ), puis défini la variable de classe suivante: __block dispatch_cancelable_block_t searchBlock;.

Mon code de recherche ressemble maintenant à ceci:

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

Remarques:

  • loadPlacesAutocompleteForInput fait partie de la bibliothèque LPGoogleFunctions
  • searchBlockDelay est défini comme suit en dehors de @implementation:

    statique CGFloat searchBlockDelay = 0,2;

15
maggix

Essayez cette magie:

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

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

Notez que cet exemple appelle une méthode appelée reload mais vous pouvez lui faire appeler la méthode que vous aimez!

118
malhal

Pour les personnes qui en ont besoin dans à partir de Swift 4:

Restez simple avec un DispatchWorkItem comme ici .


ou utilisez l'ancienne méthode 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)
}

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

Amélioration Swift 4:

En supposant que vous êtes déjà conforme à UISearchBarDelegate , ceci est une améliorée Swift 4 version de Réponse 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)
}

Le but de l'implémentation cancelPreviousPerformRequests (withTarget:) est d'empêcher l'appel continu à la reload() pour chaque modification de la barre de recherche (sans l'ajouter, si vous avez tapé "abc", reload() sera appelée trois fois en fonction du nombre de caractères ajoutés).

L'amélioration est: dans reload() la méthode a le paramètre sender qui est la barre de recherche; Ainsi, accéder à son texte - ou à l'une de ses méthodes/propriétés - serait accessible en le déclarant comme une propriété globale dans la classe.

10
Ahmad F

Un hack rapide serait comme ça:

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

Chaque fois que l'affichage du texte change, le minuteur est invalidé, ce qui l'empêche de se déclencher. Une nouvelle minuterie est créée et réglée pour se déclencher après 1 seconde. La recherche n'est mise à jour qu'après que l'utilisateur a arrêté de taper pendant 1 seconde.

10
duci9y

Solution Swift 4, plus quelques commentaires généraux:

Ce sont toutes des approches raisonnables, mais si vous voulez un comportement de recherche automatique exemplaire, vous avez vraiment besoin de deux minuteries ou répartitions distinctes.

Le comportement idéal est que 1) la recherche automatique est déclenchée périodiquement, mais 2) pas trop fréquemment (en raison de la charge du serveur, de la bande passante cellulaire et du risque de provoquer des bégaiements de l'interface utilisateur), et 3) elle se déclenche rapidement dès qu'il y a une pause dans la frappe de l'utilisateur.

Vous pouvez obtenir ce comportement avec un minuteur à plus long terme qui se déclenche dès que l'édition commence (je suggère 2 secondes) et est autorisé à s'exécuter indépendamment de l'activité ultérieure, plus un minuteur à court terme (~ 0,75 seconde) qui est réinitialisé à chaque changement. L'expiration de l'un des minuteurs déclenche la recherche automatique et réinitialise les deux minuteurs.

L'effet net est que la saisie continue génère des recherches automatiques toutes les secondes de longue période, mais une pause est garantie pour déclencher une recherche automatique en quelques secondes.

Vous pouvez implémenter ce comportement très simplement avec la classe AutosearchTimer ci-dessous. Voici comment l'utiliser:

// 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 gère son propre nettoyage lorsqu'il est libéré, il n'y a donc pas lieu de s'inquiéter à ce sujet dans votre propre code. Mais ne donnez pas à la minuterie une référence forte à vous-même ou vous créerez un cycle de référence.

L'implémentation ci-dessous utilise des minuteries, mais vous pouvez la refondre en termes d'opérations de répartition si vous préférez.

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

}
3
GSnyder

Version Swift 2.0 de la solution 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)
}
3
William T.

On peut utiliser 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;
}

En savoir plus sur Limiter l'exécution d'un bloc à l'aide de GCD

Si vous utilisez ReactiveCocoa , envisagez la méthode throttle sur RACSignal

Voici ThrottleHandler in Swift in vous êtes intéressé

2
onmyway133

Veuillez consulter le code suivant que j'ai trouvé sur les commandes de cacao. Ils envoient une demande de manière asynchrone pour récupérer les données. Peut-être qu'ils obtiennent des données locales, mais vous pouvez les essayer avec l'API distante. Envoyer une demande asynchrone sur l'API distante dans le thread d'arrière-plan. Suivez le lien ci-dessous:

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

2
Nirmit Dagly