Как ограничить поиск (в зависимости от скорости набора текста) в iOS UISearchBar?

80

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

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

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

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

благодаря

Maggix
источник
1
Я не уверен, что название правильное. То, что вы просите, называется "дребезг", а не "дроссель".
V_tredue

Ответы:

132

Попробуйте это волшебство:

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

Быстрая версия:

 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, но вы можете заставить его вызывать любой метод, который вам нравится!

Malhal
источник
это отлично работает ... не знал о методе cancelPreviousPerformRequestsWithTarget!
jesses.co.tt
пожалуйста! Это отличный шаблон, который можно использовать для самых разных вещей.
Malhal
Столько полезного! Это настоящее вуду
Маттео Пачини
2
Что касается «перезагрузки» ... Мне пришлось подумать об этом на пару дополнительных секунд ... Это относится к локальному методу, который фактически выполнит то, что вы хотите сделать, после того, как пользователь перестанет печатать в течение 0,5 секунды. Метод можно называть как угодно, например searchExecute. Благодаря!
Blalond
у меня это не работает ... он продолжает запускать функцию "перезагрузить" каждый раз при изменении буквы
Андрей
52

Для людей, которым это нужно в Swift 4 и новее :

Сделайте это просто с DispatchWorkItemподобным здесь .


или используйте старый способ 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)
}

РЕДАКТИРОВАТЬ: Версия 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")
}
VivienG
источник
1
Хороший ответ! Я просто добавил к нему небольшое улучшение, вы могли бы это проверить :)
Ahmad F
Спасибо @AhmadF, я думал сделать обновление SWIFT 4. Ты сделал это! : D
VivienG
1
Для Swift 4 используйте, DispatchWorkItemкак было предложено выше. Работает элегантнее, чем селекторы.
Тэффи
21

Улучшенный Swift 4:

Предполагая, что вы уже соответствуете требованиям UISearchBarDelegate, это улучшенная версия Swift 4 ответа 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)
}

Цель реализации cancelPreviousPerformRequests (withTarget :) - предотвратить непрерывный вызов методаreload() для каждого изменения в строке поиска (без его добавления, если вы набрали «abc», reload()будет вызываться три раза в зависимости от количества добавленных символов) .

Улучшение является: в reload()методе имеет параметр отправителя , который является панелью поиска; Таким образом, доступ к его тексту - или к любому из его метода / свойств - будет доступен с объявлением его как глобального свойства в классе.

Ахмад Ф
источник
Это действительно полезно для меня, парсинг с объектом строки поиска в селекторе
Хари Нараянан
Я только что пробовал в OBJC - (void) searchBar: (UISearchBar *) searchBar textDidChange: (NSString *) searchText {[NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector (validateText :) object: searchBar]; [самостоятельно выполнитьSelector: @selector (validateText :) withObject: searchBar afterDelay: 0.5]; }
Хари Нараянан
18

Благодаря этой ссылке я нашел очень быстрый и чистый подход. По сравнению с ответом 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;

Maggix
источник
1
Ссылка на сообщение в блоге кажется мне мертвой
jeroen
1
@jeroen, ты прав: к сожалению, похоже, что автор удалил блог со своего сайта. Репозиторий на GitHub, в котором упоминался этот блог, все еще открыт, поэтому вы можете проверить код здесь: github.com/SebastienThiebaud/dispatch_cancelable_block
maggix
код внутри searchBlock никогда не выполняется. Нужен ли еще код?
itinance
12

Быстрый взлом будет примерно таким:

- (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 секунды.

duci9y
источник
Похоже, у нас был такой же подход, и этот даже не требует дополнительного кода. Хотя requestNewDataFromServerметод должен быть изменен, чтобы получить параметр отuserInfo
maggix 05
Да, измените его в соответствии с вашими потребностями. Концепция та же.
duci9y 05
3
поскольку при таком подходе таймер никогда не запускается, я понял, что здесь отсутствует одна строка: [[NSRunLoop mainRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode];
itinance
@itinance Что ты имеешь в виду? Таймер уже находится в текущем цикле выполнения, когда вы создаете его с помощью метода в коде.
duci9y
Это быстрое и удобное решение. Вы также можете использовать это в других сетевых запросах, например, в моей ситуации, я получаю новые данные каждый раз, когда пользователь перетаскивает свою карту. Обратите внимание, что в Swift вам нужно создать экземпляр объекта таймера, вызвав метод scheduledTimer....
Гленн Посадас
5

Решение Swift 4 плюс некоторые общие комментарии:

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

Идеальное поведение состоит в том, что 1) автопоиск запускается периодически, но 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()
    }

}
Г.С.найдер
источник
3

См. Следующий код, который я нашел в элементах управления какао. Они отправляют запрос асинхронно для получения данных. Возможно, они получают данные с локального компьютера, но вы можете попробовать это с помощью удаленного API. Отправить асинхронный запрос на удаленный API в фоновом потоке. Перейдите по ссылке ниже:

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

Нирмит Даглы
источник
Привет! Наконец-то у меня было время взглянуть на предложенный вами элемент управления. Это определенно интересно, и я не сомневаюсь, что многим это принесет пользу. Однако я думаю, что нашел более короткое (и, на мой взгляд, более чистое) решение из этого сообщения в блоге, благодаря некоторому вдохновению из вашей ссылки: sebastienthiebaud.us/blog/ios/gcd/block/2014/04/09/…
maggix 05
@maggix срок действия указанной вами ссылки истек. Можете ли вы предложить любую другую ссылку.
Нирмит Даглы
Я обновляю все ссылки в этой теме. Используйте тот, что в моем ответе ниже ( github.com/SebastienThiebaud/dispatch_cancelable_block )
maggix
Также посмотрите на это, если вы используете Google Maps. Это совместимо с iOS 8 и написано на объекте-c. github.com/hkellaway/HNKGooglePlacesAutocomplete
Нирмит Дагли
3

Мы можем использовать 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метод наRACSignal

Вот ThrottleHandler в Swift, если вам интересно

onmyway133
источник
Я нахожу github.com/SebastienThiebaud/dispatch_cancelable_block/blob/… тоже полезным
onmyway133
3

Версия 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)
}
Уильям Т.
источник