Pull to refresh

Отзывчивый поиск для UITableView

Reading time4 min
Views8.7K

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

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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 14: ↑12 and ↓2+10
Comments7

Articles