Як придушити пошук (на основі швидкості набору тексту) в iOS UISearchBar?


80

У мене є частина UISearchBar частини UISearchDisplayController, яка використовується для відображення результатів пошуку як з локальної CoreData, так і з віддаленого API. Чого я хочу досягти, це "затримка" пошуку на віддаленому API. В даний час для кожного символу, набраного користувачем, надсилається запит. Але якщо користувач набирає текст особливо швидко, немає сенсу надсилати багато запитів: це допомогло б почекати, поки він перестане вводити текст. Чи є спосіб досягти цього?

Читання документації пропонує зачекати, поки користувачі явно не натиснуть на пошук, але я не вважаю це ідеальним у моєму випадку.

Проблеми з продуктивністю. Якщо пошукові операції можуть виконуватися дуже швидко, можна оновити результати пошуку, коли користувач набирає текст, застосувавши метод searchBar: textDidChange: на об'єкті делегата. Однак, якщо операція пошуку займає більше часу, слід почекати, поки користувач натисне кнопку пошуку, перш ніж розпочинати пошук у методі searchBarSearchButtonClicked:. Завжди виконуйте операції пошуку фоновий потік, щоб уникнути блокування основного потоку. Це підтримує реагування вашого додатка на користувача під час запуску пошуку та забезпечує кращу взаємодію з користувачем.

Надсилання багатьох запитів до API не є проблемою локальної продуктивності, а лише уникнення занадто високої швидкості запитів на віддаленому сервері.

Дякую


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

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


це чудово працює ... не знав про метод 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)
}

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")
}

1
Хороша відповідь! Я просто додав до нього трохи покращення, ви можете це перевірити :)
Ахмад Ф,

Дякую @AhmadF, я думав зробити оновлення SWIFT 4. Ти зробив це! : D
VivienG

1
Для Swift 4 використовуйте, DispatchWorkItemяк було запропоновано вище. Це працює елегантно, ніж селектори.
Теффі

21

Покращений Swift 4:

Якщо припустити, що ви вже відповідаєте UISearchBarDelegate, це вдосконалена версія відповіді VivienG 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() буде викликано тричі на основі кількості доданих символів) .

Поліпшення є: в 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

Завдяки цьому посиланню я знайшов дуже швидкий та чистий підхід. Порівняно з відповіддю Нірміта, у ньому відсутній "індикатор завантаження", проте він виграє за кількістю рядків коду і не вимагає додаткових елементів керування. Спочатку я додав 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 :

    статичний пошук CGFloatBlockDelay = 0,2;


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


Схоже, у нас був однаковий підхід, і цей навіть не вимагає додаткового коду. Хоча requestNewDataFromServerметод потрібно змінити, щоб отримати параметр зuserInfo
maggix

Так, модифікуйте його відповідно до своїх потреб. Концепція однакова.
duci9y

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

@maggix термін дії посилання, який ви надали, минув. Чи можете ви запропонувати будь-яке інше посилання.
Nirmit Dagly

Я оновлюю всі посилання в цій темі. Використовуйте той, що міститься у моїй відповіді нижче ( github.com/SebastienThiebaud/dispatch_cancelable_block )
maggix

Також подивіться на це, якщо ви використовуєте Google Maps. Це сумісно з iOS 8 і написано в цель-с. github.com/hkellaway/HNKGooglePlacesAutocomplete
Nirmit Dagly

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 у вас цікавить



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)
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.