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
quelle
Antworten:
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.
quelle
Für Leute, die dies ab Swift 4 benötigen :
Halte es einfach mit einem
DispatchWorkItem
Like 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") }
quelle
DispatchWorkItem
wie oben vorgeschlagen. Es funktioniert eleganter als Selektoren.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.quelle
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.h
Datei 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:
loadPlacesAutocompleteForInput
ist Teil der LPGoogleFunctions- BibliotheksearchBlockDelay
ist wie folgt definiert außerhalb@implementation
:statisches CGFloat searchBlockDelay = 0,2;
quelle
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.
quelle
requestNewDataFromServer
Methode geändert werden muss, um den Parameter von deruserInfo
scheduledTimer...
.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() } }
quelle
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
quelle
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
throttle
Methode in BetrachtRACSignal
Hier ist ThrottleHandler in Swift, an dem Sie interessiert sind
quelle
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) }
quelle