Thursday, September 27, 2012

Easy Updating of UITableView-s

When it comes to update a UITableView you have two choices. Either you reload the whole UITableView by sending a reload message or you can do differential updates by inserting, deleting and reloading rows with changed contents.

However it may be somehow difficult to always succeed in differential updates. Very often a UITableView assertion error falls down. And it can become very confusing with many sections etc...


Avoiding UITableView Assertions Errors

To avoid, these assertions errors, we have to be extra careful and:
  • Avoid changing the data source out of the main thread.
I'm not saying "don't" but avoid because you may end up with concurrent updates.
You may process and compute the updates on an other thread but the data source should be changed just right before the tableView updates. I usually do the computation on an other thread and swap (or commit) the data source at update time on the main thread.
  • Group changes (reloads/deletes/inserts) within a -[UITableView beginUpdates] / -[UITableView endUpdates] block
The reason for this is that when a an update is processed by the UITableView, it does a bit of consistency checking by making a snapshot of how many entries there were before and how many there are after. If you insert new rows, the total rows count must increase accordingly. And if you remove rows the count must decrease.

So if you need to do both deletes and inserts in the same tableview update, you must proceed like this:

[datasource commitUpdates];

[tableView beginUpdates];
      
[tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimation...];
[tableView insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimation...];

[tableView endUpdates];

Complex UITableView-s

Your project may require a big UITableView with many sections. It can be difficult to manage different data sources for each sections not to say that the UITableViewDataSource methods for cell creation and others may look very very complex with a lot of conditions or switches etc...

Enters SmartGroups

So I made these two classes MCCSmartGroup and MCCSmartGroupManager which are very easy to use and don't require much to learn. You may find them on my github profile. These class provide a good solution to this problem. They allow a much clearer implementation (with blocks!;) and also manage the updating without any additional code from yourself.

Creating a SmartGroup

A SmartGroup in my terminology is a group of rows that depends on same data source. The smart group knows how to fetch it's data and also knows how to present itself by returning a view for each row it owns (it's smart huh?). So basically, the SmartGroup is an Object that makes the link between a dataSource and a set of views. When the dataSource is updated, you may send a reload message to the SmartGroup so that it can analyze the differences and create views accordingly.

Example:
MCCSmartGroup *smartGroup = [[[MCCSmartGroup alloc]init]autorelease];

smartGroup.dataBlock = ^NSArray *{
  return self.names;
};
  
smartGroup.viewBlock = ^UIView *(NSInteger index, NSString *name) {
  return /* a UIView for this index and name */;
};

For the sake of this example, let's pretend that we display a list of names and that there is a NSMutableArray ivar containing names.

In a UITableView context, the SmartGroup is a section. Each row is a UITableViewCell returned by the smartGroup from its viewBlock.

Example in a UITableView context:
smartGroup.title = @"Names"; // Section title

smartGroup.viewBlock = ^UITableViewCell *(NSInteger index, NSString *name) {
  static NSString *identifier = @"identifier";
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
  if (!cell) { 
    cell = [[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier1]autorelease]; 
  }

  cell.textLabel.text = name;
  return cell;
};

As you can see, nothing special here it's the basic UITableView data source cell creation method in a block.

Since we want to add this SmartGroup to the UITableView we now need to introduce the MCCSmartGroupManager which manages SmartGroups.
You add SmartGroups to it. Then you set the manager as the dataSource of the UITableView and your all set.

Example:
self.manager = [[[MCCSmartGroupManager alloc]init]autorelease];
[manager addSmartGroup:smartGroup inTableView:tableView];

tableView.dataSource = manager;

This does two things. First it added the SmartGroup to the manager object and then told the manager  that it is in a UITableView context. The manager now knows how to insert/delete/reload rows when the SmartGroup is updated. From now on, you will never have to worry about rows being added, or removed or reloaded or section appearing or disappearing, all this will be handled by the manager.

Now all that is left to do is to trigger the SmartGroup update when the dataSource of the SmartGroup changes. For example, when a new name pops in. You have many options. You can either store a reference to the SmartGroup in a controller ivar and then send processUpdate message to it, or you can register for a particular update notification like this:

[[NSNotificationCenter defaultCenter]addObserverForName:@"updateNames" object:nil queue:aQueue usingBlock:^(NSNotification *note) {
    [smartGroup processUpdates];
}];

Now when you update the data source of this SmartGroup (because you added few names and removed few others) you just need to send this @"updateNames" notification and the rest of the UITableView updating is automatically handled for you. Rows will be inserted, deleted, reloaded... sections will be inserted or removed ... all with animation!

Note: You don't need to send the processUpdates message on the main thread since the MCCSmartGroupManager will dispatch_async to the main queue the tableView updates.

Be sure to check these class on my github profile here.

Cheers.

No comments:

Post a Comment