В этой статье я поделюсь с вами подходом к реализации поиска в 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;
const char *label = "ru.rinatabidullin.unique.search";
dispatch_queue_t queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
if ([searchText length]) {
weakSelf.searchProducts = [self.food productsBySearchPhrase:searchText];
weakSelf.isSearchNowProducts = YES;
}
else {
weakSelf.isSearchNowProducts = NO;
}
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData];
});
});
}
DataSource нашего контроллера заполняем не на главном потоке, иначе получим притормаживание интерфейса. После того, как данные получены, обновляем табличное представление (всегда на главном потоке).
На первый взгляд выглядит все нормально. Но при быстром вводе запроса пользователем можно добиться падения приложения:
Ошибка, как правило, следующего содержания:
Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 3 beyond bounds [0 .. 2]'
Т. е. пока таблица перерисовывается, пользователь успевает ввести новый символ в строку поиска и DataSource обновляется.
Первое решение, какое мне пришло в голову — вызывать методы поиска (обновление DataSource) только если между вводами символов проходит, к примеру, 0,3 секунды. На просторах гитхаба была найдена реализация отменяемого блока:
//
// 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;
double searchDelay = 0.3;
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
if ([searchText length]) {
weakSelf.searchProducts = [weakSelf.food productsBySearchPhrase:searchText];
weakSelf.isSearchNowProducts = YES;
}
else {
weakSelf.isSearchNowProducts = NO;
}
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData];
});
});
}
Теперь поиск работает без сбоев и падений приложения. Но есть небольшое подвисание пользовательского интерфейса:
Исправить этот недостаток поможет использование NSOperation:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
__weak ProductPickerTableViewController *weakSelf = self;
double searchDelay = 0.3;
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
[weakSelf.currentSearchOperation cancel];
weakSelf.currentSearchOperation = [NSBlockOperation blockOperationWithBlock:^{
if ([searchText length]) {
weakSelf.searchProducts = [weakSelf.food productsBySearchPhrase:searchText];
weakSelf.isSearchNowProducts = YES;
}
else {
weakSelf.isSearchNowProducts = NO;
}
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.tableView reloadData];
weakSelf.currentSearchOperation = nil;
});
}];
const char *label = "ru.rinatabidullin.unique.search";
dispatch_queue_t queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
[weakSelf.currentSearchOperation start];
});
});
}
В результате получаем отзывчивый по мнению пользователя поиск:
Автор: Watchman142