Train on cats: modifying collections and tables in iOS

image To visualize arrays of arbitrary data, Apple gave us UITableView image tables for one-dimensional visualizations and a UICollectionView collection for more complex cases. For example, in iFunny, tens of thousands of users publish and send out “memes” every day. The application constantly works with various lists: memes, users, tags, correspondence, etc.

The task of displaying a list is very common, and it is quite easy to program. However, things get complicated if this list changes dynamically. Unexpectedly catch an NSInternalInconsistencyException after the next update of the contents of the table or collection - a dubious pleasure. Let's figure out how to avoid this.

So, we have a standard task: to load and display the first packet of data about kittens and then, as you watch, load the next batch of content, adding new elements to the end of the table. The following is an example of a UITableView, but the described mechanics are also relevant for UICollectionView.

The Model object stores the full array of currently loaded data. ViewController with UITableViewDataSource Functions Builds UITableView cells. Each element of the kittens array of the model corresponds to a table cell in the 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 asks the model for the next batch of kittens. The model, after loading the content, adds them to its array and after that notifies the controller about updating the data and the need to add elements to the table.



It looks logical, and you may have met many similar implementations in different examples of working with tables. However, in practice one has to face a number of problems.

First, the code in the ViewController that inserts the next elements into the table is repeated for each table you write. Code duplication is bad, no one will argue with that?

Secondly, what will happen in a more difficult situation, when, for example, it becomes necessary to remove an element from a table or change the order of elements in an array? When performing these operations simultaneously, there is a good chance of getting an NSInternalInconsistencyException like this:
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).'

Let's look at an example from developer.apple.com (it’s true for UICollectionView, but the essence is the same) :

[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];

According to Apple documentation, cells from Data Source should be updated inside the performBatchUpdates block. In our case, updating the model (which is actually the UITableViewDataSource data source for our table) is performed non-atomically, that is, outside the block of code limited by calls to beginUpdates and endUpdates. So, based on the laconic descriptions in the Apple documentation, you can formulate 3 rules for modifying collections and tables:

  • you need to update the Data Source inside the update block of the table or collection itself;
  • while deleting some elements and adding others, first the unnecessary ones are deleted, then the missing elements are inserted;
  • You can not swap and simultaneously add / remove elements.

We figured out the theory. Now that we have formulated these principles, we will try to reflect them in the code. The following is a description of the protocol of the modifier object of a table, collection, or some other View to display multiple objects.

The modifier should be able to update, delete or insert elements in the View, and also swap cells if necessary. Moreover, each function as one of the parameters accepts a Data Source modification block so that it atomically updates both the model and the view. For example, the multipleItemsViewModifyBlock block returns an array of indices that have been updated (deleted or added) in the model array, which means that the corresponding cells must be updated (deleted or added) in the View. When calling this function, you need to take into account the sequence of calling the modification blocks: first updateBlock, then deleteBlock and finally 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<NSIndexPath *> *(^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<TRMultipleItemsViewModifierDelegate> *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<TRMoveItemInfo *> *(^)(void))moveBlock;
 
// Atomically view and model modifying without any animation
- (void)modifyNotAnimatedWithBlock:(void (^)(void))modifyBlock;
 
@end

The MultipleItemsViewModify library , which is easily installed via CocoaPods, also contains 2 implementations of the described protocol for UITableView (TRTableViewModifier) ​​and UICollectionView (TRCollectionViewModifier).

The interaction scheme between ViewController and Model of our example with kittens now looks something like this:
ViewController, in addition to the table itself, stores its viewModifier modifier, and the Model object stores a weak link to this modifier. The model, having loaded a new portion of kittens, in the main thread calls the method of animated updating of the table. The insertBlock, which is passed to this function, modifies the kitties internal array.

Using modifiers (TRCollectionViewModifier and TRTableViewModifier) ​​solves the problem of inconsistent collection modifications, tells the developer how to update the data in the array. In addition, the modifier splits the insertion, deletion, and reversal of table elements. And the amount of code in the View Controller is noticeably reduced. Not bad, right?

This, perhaps, will end. I will be glad to answer your questions and comments in the comments!

Links in the article:
Repository with an example;
→ Apple's documentation of interest to us here .