Тренируемся на кошках: модификация коллекций и таблиц в iOS

image Для визуализации массивов произвольных данных Apple дала нам таблицы UITableView для одномерных визуализаций и коллекции UICollectionView для более сложных случаев. Например, в iFunny ежедневно десятки тысяч пользователей публикуют и рассылают «мемасики». Приложение постоянно работает с различными списками: мемы, пользователи, тэги, переписки и т.д.

Задача отображения какого-либо списка весьма распространённая, и это довольно легко программируется. Однако всё существенно усложняется, если этот список динамически меняется. Неожиданно поймать NSInternalInconsistencyException после очередного обновления содержимого таблицы или коллекции — удовольствие сомнительное. Давайте разберёмся, как этого избежать.

7ecftym49kl9hchhakudvyigbds.jpeg Итак, перед нами стандартная задача: загрузить и отобразить первую пачку данных о котятах и затем по мере просмотра подгружать очередную порцию контента, добавляя новые элементы в конец таблицы. Далее рассматривается пример с UITableView, но описываемые механики актуальны и для UICollectionView.

Объект Model хранит полный массив загруженных на текущий момент данных. ViewController с функциями UITableViewDataSource строит ячейки UITableView. Каждому элементу массива котят модели соответствует ячейка таблицы во ViewController.

#pragma mark - UITableViewDataSource

-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {
    return self.model.kittiesCount;
}

-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"KittiesCell" forIndexPath:indexPath];
    NSString *kittyName = [self.model kittyAtIndex:indexPath.row];
    cell.textLabel.text = [NSString stringWithFormat:@"kitty '%@'", kittyName];
    return cell;
}


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

q8cesrgaq1tm5x40rkd8s8v7fww.png

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

Во-первых, код во ViewController, вставляющий очередные элементы в таблицу, повторяется для каждой написанной вами таблицы. Дублирование кода — это плохо, никто ведь с этим не будет спорить?

Во-вторых, что произойдёт в более сложной ситуации, когда, например, появится необходимость удалить элемент из таблицы или изменить порядок элементов в массиве? При одновременном выполнении этих операций велик шанс получить NSInternalInconsistencyException наподобие такого:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (10) must be equal to the number of rows contained in that section before the update (10), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted).'


Обратимся к примеру с developer.apple.com(он, правда, для UICollectionView, но суть одна и та же):

[self.collectionView performBatchUpdates:^{
   NSArray* itemPaths = [self.collectionView indexPathsForSelectedItems];
   // Delete the items from the data source.
   [self deleteItemsFromDataSourceAtIndexPaths:itemPaths];
   // Now delete the items from the collection view.
   [self.collectionView deleteItemsAtIndexPaths:itemPaths];
} completion:nil];


Согласно документации Apple, ячейки из Data Source следует обновлять внутри блока performBatchUpdates. В нашем случае обновление модели (фактически являющейся источником данных UITableViewDataSource для нашей таблицы) выполняется неатомарно, то есть вне блока кода, ограниченного вызовами beginUpdates и endUpdates. Итак, на основе немногословных описаний в документации Apple можно сформулировать 3 правила модификации коллекций и таблиц:

  • обновлять Data Source надо внутри блока обновления самой таблицы или коллекции;
  • при одновременном удалении одних элементов и добавлении других сначала выполняется удаление ненужных, потом вставка недостающих элементов;
  • нельзя менять местами и одновременно добавлять/удалять элементы.


С теорией разобрались. Теперь, когда мы сформулировали эти принципы, попробуем отразить их в коде. Ниже предлагается описание протокола объекта-модификатора таблицы, коллекции или какого-то другого View для отображения множества объектов.

Модификатор должен уметь обновить, удалить или вставить элементы во View, а также поменять ячейки местами, если потребуется. При этом каждая функция в качестве одного из параметров принимает блок модификации Data Source так, чтобы атомарно обновить и модель, и вид. Например, блок multipleItemsViewModifyBlock возвращает массив индексов, которые были обновлены (удалены или добавлены) в массиве модели, а значит, и во View должны быть обновлены (удалены или добавлены) соответствующие ячейки. При вызове этой функции нужно учитывать последовательность вызова блоков модификации: сначала updateBlock, затем deleteBlock и, наконец, insertBlock.

/**
 * Prototype of modification function.
 * Body of function used to perform model modifications.
 * Result of this function is array of items (their index paths)
 * that had been modified in this block and should been modified in view.
 */
typedef NSArray *(^multipleItemsViewModifyBlock)(void);
 
 
/**
 * Controller-side object that performs routine update/delete/insert
 * operations with multiple items views (like UITableView or UICollectionView).
 * Object allows performing safe modifications of view and model atomically
 * that prevents from inconsistency crashes.
 */
@protocol TRMultipleItemsViewModifierProtocol
 
@required
@property (nonatomic, weak) NSObject *delegate;
 
/**
 * Method performs animated model and view modifications atomically.
 * Modification order:
 * 1. existing items are updating
 * 2. exhausted items are deleting
 * 3. new items are inserting.
 */
- (void)modifyAnimatedWithUpdateBlock:(multipleItemsViewModifyBlock)updateBlock
                          deleteBlock:(multipleItemsViewModifyBlock)deleteBlock
                          insertBlock:(multipleItemsViewModifyBlock)insertBlock;
 
// Move cells in view
- (void)modifyAnimatedWithMoveBlock:(NSArray *(^)(void))moveBlock;
 
// Atomically view and model modifying without any animation
- (void)modifyNotAnimatedWithBlock:(void (^)(void))modifyBlock;
 
@end


В библиотеке MultipleItemsViewModify, которая легко ставится через CocoaPods, содержится также 2 реализации описанного протокола для UITableView (TRTableViewModifier) и UICollectionView (TRCollectionViewModifier).

Схема взаимодействия ViewController и Model нашего примера с котятами выглядит теперь примерно так:

he1fow3_s61kvprg5xgz4lpdsrq.png

ViewController, помимо самой таблицы, хранит её модификатор viewModifier, а объект Model хранит слабую ссылку на этот модификатор. Модель, загрузив новую порцию котят, в главном потоке вызывает метод анимационного обновления таблицы. Блок insertBlock, который передаётся в эту функцию, выполняет модификацию внутреннего массива kitties.

Использование модификаторов (TRCollectionViewModifier и TRTableViewModifier) решает проблему несогласованных модификаций коллекций, подсказывает разработчику порядок обновления данных в массиве. Помимо этого, модификатор разделяет вставку, удаление и перемену элементов таблицы местами. Да и количество кода во View Controller заметно поубавилось. Неплохо, правда?

На этом, пожалуй, и закончим. Буду рад ответить на ваши вопросы и замечания в комментариях!

Ссылки в статье:

→ Репозиторий с примером;
→ Интересующая нас документация от Apple здесь.

© Habrahabr.ru