Как дросселировать поиск (на основе скорости ввода) в iOS UISearchBar?



У меня есть uisearchbar часть UISearchDisplayController, который используется для отображения результатов поиска из локального CoreData и удаленного API.
То, что я хочу достичь, - это "задержка" поиска на удаленном API. В настоящее время, для каждого символа, введенного пользователем, посылается запрос. Но если пользователь печатает особенно быстро, то нет смысла отправлять много запросов: это помогло бы дождаться, пока он перестанет печатать.
Есть ли способ достичь этого?



чтение документация предлагает подождать, пока пользователи явно не нажмут на поиск, но я не считаю его идеальным в моем случае.




проблемы с производительностью. Если поисковые операции могут быть проведены очень
быстро, можно обновить результаты поиска как пользователь
ввод текста путем реализации метода searchBar:textDidChange: на панели поиска
объект делегата. Однако, если операция поиска занимает больше времени, вы
следует подождать, пока пользователь не нажмет кнопку поиска перед началом этот
поиск в searchBarSearchButtonClicked: метод. Всегда выполнять
операции поиска фоновый поток, чтобы избежать блокировки основного
нитка. Это позволяет вашему приложению реагировать на пользователя во время поиска
работает и обеспечивает лучший пользовательский опыт.




отправка большого количества запросов в API-это не проблема локальной производительности, а только предотвращение слишком высокой скорости запросов на удаленном сервере.



спасибо

548   9  

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 метод on RACSignal

здесь 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

    Ничего не найдено.