Wie drossle ich die Suche (basierend auf der Schreibgeschwindigkeit) in iOS UISearchBar?

80

Ich habe einen UISearchBar-Teil eines UISearchDisplayControllers, der zum Anzeigen von Suchergebnissen sowohl von lokalen CoreData- als auch von Remote-APIs verwendet wird. Was ich erreichen möchte, ist die "Verzögerung" der Suche auf der Remote-API. Derzeit wird für jedes vom Benutzer eingegebene Zeichen eine Anforderung gesendet. Wenn der Benutzer jedoch besonders schnell tippt, ist es nicht sinnvoll, viele Anfragen zu senden: Es wäre hilfreich zu warten, bis er aufgehört hat zu tippen. Gibt es einen Weg, dies zu erreichen?

Wenn Sie die Dokumentation lesen, sollten Sie warten, bis die Benutzer explizit auf die Suche tippen, aber ich finde sie in meinem Fall nicht ideal.

Performance-Probleme. Wenn Suchvorgänge sehr schnell ausgeführt werden können, können die Suchergebnisse während der Eingabe durch den Benutzer aktualisiert werden, indem die Methode searchBar: textDidChange: für das Delegatenobjekt implementiert wird. Wenn ein Suchvorgang jedoch länger dauert, sollten Sie warten, bis der Benutzer auf die Schaltfläche Suchen tippt, bevor Sie mit der Suche in der Methode searchBarSearchButtonClicked: beginnen. Führen Sie Suchvorgänge immer als Hintergrundthread durch, um zu vermeiden, dass der Hauptthread blockiert wird. Dadurch reagiert Ihre App während der Suche auf den Benutzer und bietet eine bessere Benutzererfahrung.

Das Senden vieler Anforderungen an die API ist kein Problem der lokalen Leistung, sondern nur das Vermeiden einer zu hohen Anforderungsrate auf dem Remoteserver.

Vielen Dank

Maggix
quelle
1
Ich bin mir nicht sicher, ob der Titel korrekt ist. Was Sie verlangen, heißt "entprellen", nicht "drosseln".
Vitto_tredue

Antworten:

129

Versuchen Sie diese 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];
}

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

Beachten Sie, dass in diesem Beispiel eine Methode namens reload aufgerufen wird. Sie können sie jedoch dazu bringen, eine beliebige Methode aufzurufen.

malhal
quelle
Das funktioniert großartig ... wusste nichts über die Methode cancelPreviousPerformRequestsWithTarget!
jesses.co.tt
Bitte! Es ist ein großartiges Muster und kann für alle Arten von Dingen verwendet werden.
Malhal
So viel nützlich! Dies ist der echte Voodoo
Matteo Pacini
2
In Bezug auf "Nachladen" ... musste ich ein paar Sekunden länger darüber nachdenken ... Das bezieht sich auf die lokale Methode, die tatsächlich die Dinge ausführt, die Sie tun möchten, nachdem der Benutzer für 0,5 Sekunden aufgehört hat zu tippen. Die Methode kann wie searchExecute aufgerufen werden. Vielen Dank!
Blalond
das funktioniert bei mir nicht ... es führt weiterhin die "reload" -Funktion jedes Mal aus, wenn ein Brief geändert wird
Andrey
52

Für Leute, die dies ab Swift 4 benötigen :

Halte es einfach mit einem DispatchWorkItemLike hier .


oder verwenden Sie den alten Obj-C-Weg:

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")
}
VivienG
quelle
1
Gute Antwort! Ich habe gerade eine kleine Verbesserung hinzugefügt, Sie könnten es überprüfen :)
Ahmad F
Danke @AhmadF, ich habe überlegt, ein SWIFT 4-Update durchzuführen. Du hast es geschafft! : D
VivienG
1
Verwenden Sie für Swift 4 DispatchWorkItemwie oben vorgeschlagen. Es funktioniert eleganter als Selektoren.
Teffi
21

Verbessertes Swift 4:

Vorausgesetzt, Sie sind bereits konform UISearchBarDelegate, handelt es sich um eine verbesserte Swift 4-Version der Antwort von 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)
}

Der Zweck der Implementierung von cancelPreviousPerformRequests (withTarget :) besteht darin, das kontinuierliche Aufrufen von reload()bei jeder Änderung der Suchleiste zu verhindern (ohne Hinzufügen, wenn Sie "abc" eingegeben haben, reload()wird dies basierend auf der Anzahl der hinzugefügten Zeichen dreimal aufgerufen). .

Die Verbesserung ist: in reload()Methode hat den Absenderparameter, der die Suchleiste ist; Der Zugriff auf seinen Text - oder auf eine seiner Methoden / Eigenschaften - wäre somit möglich, wenn er als globale Eigenschaft in der Klasse deklariert wird.

Ahmad F.
quelle
Es ist wirklich hilfreich für mich, mit dem Objekt der Suchleiste im Selektor zu analysieren
Hari Narayanan
Ich habe es gerade in OBJC versucht - (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

Dank dieses Links fand ich einen sehr schnellen und sauberen Ansatz. Im Vergleich zu Nirmit's Antwort fehlt der "Ladeindikator", er gewinnt jedoch in Bezug auf die Anzahl der Codezeilen und erfordert keine zusätzlichen Steuerelemente. Ich habe die dispatch_cancelable_block.hDatei zuerst zu meinem Projekt hinzugefügt (aus diesem Repo ) und dann die folgende Klassenvariable definiert : __block dispatch_cancelable_block_t searchBlock;.

Mein Suchcode sieht jetzt so aus:

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

Anmerkungen:

  • Das loadPlacesAutocompleteForInputist Teil der LPGoogleFunctions- Bibliothek
  • searchBlockDelayist wie folgt definiert außerhalb @implementation:

    statisches CGFloat searchBlockDelay = 0,2;

Maggix
quelle
1
Der Link zum Blog-Beitrag erscheint mir tot
jeroen
1
@jeroen du hast recht: leider sieht es so aus, als hätte der autor den blog von seiner website entfernt. Das Repository auf GitHub, das auf dieses Blog verwiesen hat, ist noch aktiv. Sie können den Code hier überprüfen: github.com/SebastienThiebaud/dispatch_cancelable_block
maggix
Der Code im searchBlock wird niemals ausgeführt. Ist mehr Code erforderlich?
itinance
12

Ein schneller Hack wäre so:

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

Jedes Mal, wenn sich die Textansicht ändert, wird der Timer ungültig, sodass er nicht ausgelöst wird. Ein neuer Timer wird erstellt und nach 1 Sekunde ausgelöst. Die Suche wird erst aktualisiert, nachdem der Benutzer 1 Sekunde lang aufgehört hat zu tippen.

duci9y
quelle
Sieht so aus, als hätten wir den gleichen Ansatz gewählt, und für diesen ist nicht einmal zusätzlicher Code erforderlich. Obwohl die requestNewDataFromServerMethode geändert werden muss, um den Parameter von deruserInfo
Maggix
Ja, ändern Sie es entsprechend Ihren Anforderungen. Das Konzept ist das gleiche.
Duci9y
3
Da der Timer bei diesem Ansatz nie ausgelöst wird, habe ich herausgefunden, dass hier eine Zeile fehlt: [[NSRunLoop mainRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode];
itinance
@itinance Was meinst du? Der Timer befindet sich bereits in der aktuellen Ausführungsschleife, wenn Sie ihn mit der Methode im Code erstellen.
Duci9y
Dies ist eine schnelle und saubere Lösung. Sie können dies auch in Ihren anderen Netzwerkanforderungen verwenden. In meiner Situation rufe ich jedes Mal neue Daten ab, wenn der Benutzer seine Karte zieht. Nur eine Anmerkung, dass Sie in Swift Ihr Timer-Objekt durch Aufrufen von instanziieren möchten scheduledTimer....
Glenn Posadas
5

Swift 4-Lösung sowie einige allgemeine Kommentare:

Dies sind alles vernünftige Ansätze, aber wenn Sie ein beispielhaftes Autosuchverhalten wünschen, benötigen Sie wirklich zwei separate Timer oder Dispatches.

Das ideale Verhalten ist, dass 1) die Autosuche regelmäßig ausgelöst wird, 2) jedoch nicht zu häufig (aufgrund der Serverlast, der Mobilfunkbandbreite und des Potenzials, UI-Ruckler zu verursachen) und 3) schnell ausgelöst wird, sobald eine Pause eintritt die Eingabe des Benutzers.

Sie können dieses Verhalten mit einem längerfristigen Timer erreichen, der ausgelöst wird, sobald die Bearbeitung beginnt (ich schlage vor, 2 Sekunden) und unabhängig von späteren Aktivitäten ausgeführt werden kann, sowie einem kurzfristigen Timer (~ 0,75 Sekunden), der bei jedem zurückgesetzt wird Veränderung. Das Ablaufen eines Timers löst die Autosuche aus und setzt beide Timer zurück.

Der Nettoeffekt besteht darin, dass die kontinuierliche Eingabe alle Sekunden eine automatische Suche ergibt, eine Pause jedoch garantiert eine automatische Suche innerhalb von Sekunden in kurzer Zeit auslöst.

Sie können dieses Verhalten sehr einfach mit der folgenden AutosearchTimer-Klasse implementieren. So verwenden Sie es:

// 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...
}

Der AutosearchTimer führt beim Freigeben eine eigene Bereinigung durch, sodass Sie sich in Ihrem eigenen Code keine Sorgen machen müssen. Geben Sie dem Timer jedoch keinen starken Bezug zu sich selbst, da sonst ein Referenzzyklus erstellt wird.

Die folgende Implementierung verwendet Timer, aber Sie können sie in Bezug auf Versandvorgänge neu formulieren, wenn Sie dies bevorzugen.

// 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
quelle
3

Bitte beachten Sie den folgenden Code, den ich auf Kakaokontrollen gefunden habe. Sie senden asynchron eine Anfrage, um die Daten abzurufen. Möglicherweise erhalten sie Daten von lokal, aber Sie können es mit der Remote-API versuchen. Senden Sie eine asynchrone Anforderung auf der Remote-API im Hintergrundthread. Folgen Sie dem folgenden Link:

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

Nirmit Dagly
quelle
Hallo! Ich hatte endlich Zeit, mir Ihre vorgeschlagene Kontrolle anzusehen. Es ist definitiv interessant und ich habe keinen Zweifel, dass viele davon profitieren werden. Ich denke jedoch, dass ich in diesem Blog-Beitrag eine kürzere (und meiner Meinung nach sauberere) Lösung gefunden habe, dank einiger Inspiration von Ihrem Link: sebastienthiebaud.us/blog/ios/gcd/block/2014/04/09/…
Maggix
@maggix Der von Ihnen angegebene Link ist jetzt abgelaufen. Können Sie einen anderen Link vorschlagen?
Nirmit Dagly
Ich aktualisiere alle Links in diesem Thread. Verwenden Sie die in meiner Antwort unten ( github.com/SebastienThiebaud/dispatch_cancelable_block )
Maggix
Sehen Sie sich dies auch an, wenn Sie Google Maps verwenden. Dies ist kompatibel mit iOS 8 und in Objective-C geschrieben. github.com/hkellaway/HNKGooglePlacesAutocomplete
Nirmit Dagly
3

Wir können benutzen 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;
}

Weitere Informationen zum Drosseln einer Blockausführung mit GCD

Wenn Sie ReactiveCocoa verwenden , ziehen Sie die throttleMethode in BetrachtRACSignal

Hier ist ThrottleHandler in Swift, an dem Sie interessiert sind

onmyway133
quelle
3

Swift 2.0-Version der NSTimer-Lösung:

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.
quelle