В этой статье я поделюсь с вами подходом к реализации поиска в 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)
monarch509
01.08.2016 12:48+2Прошу прощения у автора, но здесь допущены ошибки синхронизации данные — а точнее доступа к ним… Идея рабочая, но…
DnV
01.08.2016 13:05+3Какой же это отзывчивый поиск, если вы собственноручно задержку поставили? Причём исправляя таким костылём свою же ошибку обновления таблицы. Получилось образцовое руководство как не надо делать.
Watchman142
01.08.2016 22:00Признаю ошибку, обновил статью. Задержка все же для других целей предназначалась, надеюсь в обновленной статье смысл её мне удалось передать лучше.
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]; }
silvansky
А если перейти на
ReactiveCocoa
, то можно обойтись меньшим количеством кода и при этом добиться большей стабильности.