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

Итак, у нас есть таблица с UISearchBar для поиска. DataSource’ом в данном примере будет выступать БД SQLite (но это также может быть внешний источник данных с обращением по API, например). БД содержит много записей (несколько тысяч), поиск по ней может идти порядка 0,5 секунд.

Для того, чтобы динамически формировать поисковую выдачу по мере ввода пользователем запроса, нужно реализовать метод -(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText делегата UISearchBar:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    __weak ProductPickerTableViewController *weakSelf = self;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        const char *label = "ru.example.unique.search";
        weakSelf.searchDispatchQueue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL);
    });
    
    dispatch_async(self.searchDispatchQueue, ^{
        NSArray *searchProducts = nil;
        
        if ([searchText length]) {
            searchProducts = [self.food productsBySearchPhrase:searchText];
        }
                
        dispatch_async(dispatch_get_main_queue(), ^{
            weakSelf.searchProducts = searchProducts;
            [weakSelf.tableView reloadData];
        });
    });
}


DataSource нашего контроллера заполняем не на главном потоке, иначе получим притормаживание интерфейса. После того, как данные получены, обновляем табличное представление (всегда на главном потоке).

В подобном подходе есть один недостаток — при каждом изменении строки поиска будет вызван метод поиска по БД (или отправлен сетевой запрос по API), что совершенно необязательно, когда пользователь печатает запрос достаточно быстро или удаляет его с зажатой клавишей backspace.



Первое решение, какое мне пришло в голову — вызывать методы поиска (обновление DataSource) только если между вводами символов проходит, к примеру, 0,1 секунды. На просторах гитхаба была найдена реализация отменяемого блока:

//
//  dispatch_cancelable_block.h
//  sebastienthiebaud.us
//
//  Created by Sebastien Thiebaud on 4/9/14.
//  Copyright (c) 2014 Sebastien Thiebaud. All rights reserved.
//

typedef void(^dispatch_cancelable_block_t)(BOOL cancel);

NS_INLINE dispatch_cancelable_block_t dispatch_after_delay(NSTimeInterval delay, dispatch_block_t block) {
    if (block == nil) {
        return nil;
    }
    
    // First we have to create a new dispatch_cancelable_block_t and we also need to copy the block given (if you want more explanations about the __block storage type, read this: https://developer.apple.com/library/ios/documentation/cocoa/conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW6
    __block dispatch_cancelable_block_t cancelableBlock = nil;
    __block dispatch_block_t originalBlock = [block copy];
    
    // This block will be executed in NOW() + delay
    dispatch_cancelable_block_t delayBlock = ^(BOOL cancel){
        if (cancel == NO && originalBlock) {
            originalBlock();
        }
        
        // We don't want to hold any objects in the memory
        originalBlock = nil;
    };
    
    cancelableBlock = [delayBlock copy];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        // We are now in the future (NOW() + delay). It means the block hasn't been canceled so we can execute it
        if (cancelableBlock) {
            cancelableBlock(NO);
            cancelableBlock = nil;
        }
    });
    
    return cancelableBlock;
}

NS_INLINE void cancel_block(dispatch_cancelable_block_t block) {
    if (block == nil) {
        return;
    }
    
    block(YES);
    block = nil;
}


Используя эту реализацию отменяемого блока, можно переписать метод делегата UISearchBar в следующем виде:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    __weak ProductPickerTableViewController *weakSelf = self;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        static const char *label = "ru.example.unique.search";
        weakSelf.searchDispatchQueue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL);
    });
    
    double searchDelay = 0.1;
    
    if (self.searchBlock != nil) {
        //We cancel the currently scheduled block
        cancel_block(self.searchBlock);
    }
    self.searchBlock = dispatch_after_delay(searchDelay, ^{
        //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
        
        dispatch_async(self.searchDispatchQueue, ^{
            NSArray *searchProducts = nil;
            
            if ([searchText length]) {
                searchProducts = [self.food productsBySearchPhrase:searchText];
            }
            
            dispatch_async(dispatch_get_main_queue(), ^{
                weakSelf.searchProducts = searchProducts;
                [weakSelf.tableView reloadData];
            });
        });
    });
}


Переменная searchDelay соответствует интервалу времени между вводом (или удалением) двух символов в строке поиска. 0,1 сек будет достаточно, чтобы не вызывать многократно методы поиска при стирании строки поиска клавишей backspace, 0,2...0,3 сек достаточно для быстрого ввода запроса.

В результате получаем отзывчивый по мнению пользователя поиск:

Поделиться с друзьями
-->

Комментарии (7)


  1. silvansky
    01.08.2016 12:30
    -1

    А если перейти на ReactiveCocoa, то можно обойтись меньшим количеством кода и при этом добиться большей стабильности.


  1. monarch509
    01.08.2016 12:48
    +2

    Прошу прощения у автора, но здесь допущены ошибки синхронизации данные — а точнее доступа к ним… Идея рабочая, но…


    1. Watchman142
      01.08.2016 21:57

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


  1. DnV
    01.08.2016 13:05
    +3

    Какой же это отзывчивый поиск, если вы собственноручно задержку поставили? Причём исправляя таким костылём свою же ошибку обновления таблицы. Получилось образцовое руководство как не надо делать.


    1. Watchman142
      01.08.2016 22:00

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


  1. ajjnix
    01.08.2016 22:33

    Delete


  1. zyukin
    05.08.2016 15:42

    Есть более элегантное решение…

    - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
        SEL selector = @selector(perfomSearch:);
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:selector object:nil];
        [self performSelector:selector withObject:searchText afterDelay:0.01];
    }