Как дросселировать поиск (на основе скорости ввода) в iOS UISearchBar?
У меня есть uisearchbar часть UISearchDisplayController, который используется для отображения результатов поиска из локального CoreData и удаленного API.
То, что я хочу достичь, - это "задержка" поиска на удаленном API. В настоящее время, для каждого символа, введенного пользователем, посылается запрос. Но если пользователь печатает особенно быстро, то нет смысла отправлять много запросов: это помогло бы дождаться, пока он перестанет печатать.
Есть ли способ достичь этого?
чтение документация предлагает подождать, пока пользователи явно не нажмут на поиск, но я не считаю его идеальным в моем случае.
проблемы с производительностью. Если поисковые операции могут быть проведены очень
быстро, можно обновить результаты поиска как пользователь
ввод текста путем реализации метода searchBar:textDidChange: на панели поиска
объект делегата. Однако, если операция поиска занимает больше времени, вы
следует подождать, пока пользователь не нажмет кнопку поиска перед началом этот
поиск в searchBarSearchButtonClicked: метод. Всегда выполнять
операции поиска фоновый поток, чтобы избежать блокировки основного
нитка. Это позволяет вашему приложению реагировать на пользователя во время поиска
работает и обеспечивает лучший пользовательский опыт.
отправка большого количества запросов в API-это не проблема локальной производительности, а только предотвращение слишком высокой скорости запросов на удаленном сервере.
спасибо
9 ответов:
попробуйте эту магию:
-(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]; }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) }обратите внимание, что в этом примере вызывается метод reload, но вы можете заставить его вызвать любой метод, который вам нравится!
для людей, которым это нужно в Свифт
держите его простым с
DispatchWorkItemкак здесь:https://stackoverflow.com/a/48666001/308315или использовать старый способ 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 версия
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") }
спасибо этой ссылке, я нашел очень быстрый и чистый подход. По сравнению с ответом Nirmit ему не хватает "индикатора загрузки", однако он выигрывает по количеству строк кода и не требует дополнительных элементов управления. Я сначала добавил
dispatch_cancelable_block.hфайл в мой проект (от этот РЕПО), затем определяется следующая переменная класса:__block dispatch_cancelable_block_t searchBlock;.мой код поиска теперь выглядит так:
- (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]; }); }Примечания:
- в
loadPlacesAutocompleteForInputявляется частью LPGoogleFunctions библиотека
searchBlockDelayопределяется следующим образом вне@implementation:статический CGFloat searchBlockDelay = 0.2;
быстрый хак будет выглядеть так:
- (void)textViewDidChange:(UITextView *)textView { static NSTimer *timer; [timer invalidate]; timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(requestNewDataFromServer) userInfo:nil repeats:NO]; }каждый раз, когда текстовое представление изменяется, таймер становится недействительным, в результате чего он не срабатывает. Новый таймер создается и запускается через 1 секунду. Поиск обновляется только после того, как пользователь перестает печатать за 1 секунду.
Улучшена Swift 4:
предполагая, что вы уже соответствует
UISearchBarDelegate, это улучшение 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) }цель реализации cancelPreviousPerformRequests (withTarget:) это предотвратить непрерывный вызов
reload()для каждого изменения в строке поиска (без добавления его, если вы набрали "abc",reload()будет вызываться три раза на основе количество добавленных символов).The улучшение: в
reload()метод имеет параметр sender, который является строкой поиска; таким образом, доступ к его тексту-или любому из его методов/свойств - будет доступен с объявлением его как глобального свойства в классе.
Swift 2.0 версия решения 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) }
пожалуйста, смотрите следующий код, который я нашел на cocoa controls. Они посылают запрос асинхронно, чтобы получить данные. Может быть, они получают данные из локального, но вы можете попробовать его с помощью удаленного API. Отправить асинхронный запрос на удаленный API в фоновом потоке. Перейдите по ссылке ниже:
https://www.cocoacontrols.com/controls/jcautocompletingsearch
можно использовать
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; }подробнее о дросселирование выполнения блока с помощью GCD
если вы используете ReactiveCocoa, считать
throttleметод onRACSignalздесь ThrottleHandler в Swift в тебе интересно
Swift 4 решение, а также некоторые общие замечания:
Это все разумные подходы, но если вы хотите примерное поведение автоисследования, вам действительно нужны два отдельных таймера или диспетчеры.
идеальное поведение заключается в том, что 1) autosearch запускается периодически, но 2) не слишком часто (из-за нагрузки на сервер, пропускной способности сотовой связи и возможности вызвать заикание пользовательского интерфейса), и 3) он запускается быстро, как только возникает пауза в работе пользователя напечатать.
вы можете добиться такого поведения с помощью одного долгосрочного таймера, который срабатывает, как только начинается редактирование (я предлагаю 2 секунды) и разрешается запускать независимо от более поздней активности, а также один краткосрочный таймер (~0,75 секунды), который сбрасывается при каждом изменении. Истечение срока действия любого таймера запускает автоматический поиск и сбрасывает оба таймера.
чистый эффект заключается в том, что непрерывная типизация дает автозахваты каждые длительные секунды, но пауза гарантированно вызовет автопоиск в течение короткого периода секунд.
вы можете реализовать это поведение очень просто с помощью класса AutosearchTimer ниже. Вот как это использовать:
// 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 обрабатывает свою собственную очистку при освобождении, поэтому нет необходимости беспокоиться об этом в вашем собственном коде. Но не давайте таймеру сильную ссылку на себя, или вы создадите эталонный цикл.
реализация ниже использует таймеры, но вы можете изменить его с точки зрения диспетчерское если хочешь.
// 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() } }
Comments