Mam część UISearchBar elementu UISearchDisplayController, która służy do wyświetlania wyników wyszukiwania zarówno z lokalnego CoreData, jak i zdalnego interfejsu API. Chcę osiągnąć „opóźnienie” wyszukiwania w zdalnym API. Obecnie dla każdego wpisanego przez użytkownika znaku wysyłane jest żądanie. Ale jeśli użytkownik pisze szczególnie szybko, wysyłanie wielu żądań nie ma sensu: pomogłoby poczekać, aż przestanie pisać. Czy jest na to sposób?
Czytanie dokumentacji sugeruje zaczekanie, aż użytkownicy wyraźnie dotkną wyszukiwania, ale nie uważam tego za idealne w moim przypadku.
Problemy z wydajnością. Jeśli operacje wyszukiwania mogą być wykonywane bardzo szybko, możliwe jest aktualizowanie wyników wyszukiwania w trakcie wpisywania przez użytkownika poprzez zaimplementowanie metody searchBar: textDidChange: w obiekcie delegata. Jeśli jednak operacja wyszukiwania zajmuje więcej czasu, przed rozpoczęciem wyszukiwania w metodzie searchBarSearchButtonClicked: należy poczekać, aż użytkownik naciśnie przycisk Wyszukaj. Zawsze wykonuj operacje wyszukiwania w wątku w tle, aby uniknąć blokowania wątku głównego. Dzięki temu Twoja aplikacja reaguje na użytkownika podczas wyszukiwania i zapewnia lepsze wrażenia użytkownika.
Wysyłanie wielu żądań do API nie jest problemem z lokalną wydajnością, a jedynie z unikaniem zbyt dużej szybkości żądań na zdalnym serwerze.
Dzięki
źródło
Odpowiedzi:
Spróbuj tej magii:
- (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]; }
Wersja 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) }
Zwróć uwagę, że ten przykład wywołuje metodę o nazwie reload, ale możesz sprawić, by wywoływała dowolną metodę!
źródło
Dla osób, które potrzebują tego w Swift 4 i nowszych :
Zachowaj prostotę z
DispatchWorkItem
polubieniem tutaj .lub użyj starego sposobu 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) }
EDYCJA: Wersja SWIFT 3
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") }
źródło
DispatchWorkItem
zgodnie z pierwszą sugestią powyżej. Działa elegancko niż selektory.Ulepszony Swift 4:
Zakładając, że już się dostosowujesz
UISearchBarDelegate
, jest to ulepszona wersja odpowiedzi VivienG w wersji Swift 4 :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) }
Celem implementacji cancelPreviousPerformRequests (withTarget :) jest zapobieżenie ciągłemu wywoływaniu
reload()
dla każdej zmiany w pasku wyszukiwania (bez jego dodawania, jeśli wpiszesz "abc",reload()
zostanie wywołane trzykrotnie na podstawie liczby dodanych znaków) .Poprawa jest: w
reload()
sposób ma parametr nadawcy czyli pasek wyszukiwania; W ten sposób dostęp do jego tekstu - lub dowolnej metody / właściwości - byłby dostępny po zadeklarowaniu go jako właściwości globalnej w klasie.źródło
Dzięki temu linkowi znalazłem bardzo szybkie i czyste podejście. W porównaniu z odpowiedzią Nirmit nie ma „wskaźnika ładowania”, jednak wygrywa pod względem liczby wierszy kodu i nie wymaga dodatkowych kontroli. Najpierw dodałem
dispatch_cancelable_block.h
plik do mojego projektu (z tego repozytorium ), a następnie zdefiniowałem następującą zmienną klasy:__block dispatch_cancelable_block_t searchBlock;
.Mój kod wyszukiwania wygląda teraz tak:
- (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]; }); }
Uwagi:
loadPlacesAutocompleteForInput
Jest częścią LPGoogleFunctions bibliotekisearchBlockDelay
jest zdefiniowany w następujący sposób poza@implementation
:static CGFloat searchBlockDelay = 0,2;
źródło
Szybki hack wyglądałby tak:
- (void)textViewDidChange:(UITextView *)textView { static NSTimer *timer; [timer invalidate]; timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(requestNewDataFromServer) userInfo:nil repeats:NO]; }
Za każdym razem, gdy zmienia się widok tekstu, licznik czasu jest unieważniany, co powoduje, że nie uruchamia się. Tworzony jest nowy zegar, który uruchamia się po 1 sekundzie. Wyszukiwanie jest aktualizowane tylko wtedy, gdy użytkownik przestanie pisać na 1 sekundę.
źródło
requestNewDataFromServer
metoda wymaga modyfikacji, aby uzyskać parametr zuserInfo
scheduledTimer...
.Rozwiązanie Swift 4 oraz kilka ogólnych uwag:
Są to wszystkie rozsądne podejścia, ale jeśli chcesz wzorowego zachowania automatycznego wyszukiwania, naprawdę potrzebujesz dwóch oddzielnych liczników czasu lub komunikatów.
Idealnym zachowaniem jest to, że 1) automatyczne wyszukiwanie jest uruchamiane okresowo, ale 2) niezbyt często (ze względu na obciążenie serwera, przepustowość sieci komórkowej i możliwość powodowania zacinania się interfejsu użytkownika) oraz 3) uruchamia się szybko, gdy tylko nastąpi przerwa w wpisywanie przez użytkownika.
Możesz osiągnąć to zachowanie za pomocą jednego długoterminowego timera, który uruchamia się zaraz po rozpoczęciu edycji (sugeruję 2 sekundy) i może działać niezależnie od późniejszej aktywności, plus jeden krótkoterminowy zegar (~ 0,75 sekundy), który jest resetowany co zmiana. Wygaśnięcie któregokolwiek z liczników uruchamia automatyczne wyszukiwanie i resetuje oba liczniki.
Efektem netto jest to, że ciągłe wpisywanie powoduje automatyczne przeszukiwanie co długie sekundy, ale pauza gwarantuje uruchomienie automatycznego wyszukiwania w ciągu krótkich sekund.
Możesz zaimplementować to zachowanie w bardzo prosty sposób za pomocą poniższej klasy AutosearchTimer. Oto jak z niego korzystać:
// 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 obsługuje własne czyszczenie po zwolnieniu, więc nie musisz się o to martwić w swoim własnym kodzie. Ale nie podawaj timera silnego odniesienia do siebie, bo utworzysz cykl odniesienia.
Poniższa implementacja wykorzystuje liczniki czasu, ale jeśli wolisz, możesz ją przekształcić pod kątem operacji wysyłkowych.
// 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() } }
źródło
Zobacz poniższy kod, który znalazłem na kontrolkach kakao. Wysyłają żądanie asynchronicznie w celu pobrania danych. Być może pobierają dane z lokalnego, ale możesz spróbować ze zdalnym interfejsem API. Wyślij żądanie asynchroniczne do zdalnego interfejsu API w wątku w tle. Postępuj zgodnie z poniższym linkiem:
https://www.cocoacontrols.com/controls/jcautocompletingsearch
źródło
Możemy użyć
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; }
Więcej o ograniczaniu wykonywania bloków za pomocą GCD
Jeśli używasz ReactiveCocoa , rozważ
throttle
metodę onRACSignal
Oto ThrottleHandler w Swift, który Cię interesuje
źródło
Wersja Swift 2.0 rozwiązania 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) }
źródło
Możesz używać
DispatchWorkItem
z Swift 4.0 lub nowszym. To dużo łatwiejsze i ma sens.Możemy wykonać wywołanie API, gdy użytkownik nie pisał przez 0,25 sekundy.
class SearchViewController: UIViewController, UISearchBarDelegate { // We keep track of the pending work item as a property private var pendingRequestWorkItem: DispatchWorkItem? func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { // Cancel the currently pending item pendingRequestWorkItem?.cancel() // Wrap our request in a work item let requestWorkItem = DispatchWorkItem { [weak self] in self?.resultsLoader.loadResults(forQuery: searchText) } // Save the new work item and execute it after 250 ms pendingRequestWorkItem = requestWorkItem DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250), execute: requestWorkItem) } }
Możesz przeczytać cały artykuł na ten temat tutaj
źródło