Make your UIViewController Awesynchronous!

Welcome back for the conclusion of this 3 part series! In part 1 we learnt how to slim down our view controllers to a manageable size. In part 2 we learnt how to strip out all the non-ui related logic. This time we are going to focus solely on the UITableView and how we can properly handle multiple asynchronous operations in its cells for maximum performance!

As always we will be starting with our project from last time.

UPDATE: I have finally gotten around to turning the concept behind this article into a framework you can use with your new or existing table/collection views. It's called IKAsyncable

What's making our table's scrolling so bad?

Our current implementation is very bad... our UITableViewCells are performing an expensive operation, and on the main thread no less!

The following code is currently in the setup method of our cell:

NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:data.url]];  
UIImage *image = [UIImage imageWithData:imageData];  

This code is synchronously downloading our image as an NSData and turning it into a UIImage on the main thread.

A Naive Solution

"Too easy!", I hear you say... we'll just wrap it in a warm fuzzy GCD blanket:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
    NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:data.url]];
    UIImage *image = [UIImage imageWithData:imageData];
    dispatch_async(dispatch_get_main_queue(), ^{
        self.cellImage.image = image;
    });
});

...and you would be right! kinda... This would definitely work, in fact if you change the code and run it now the scrolling would be fantastic! There's no image caching to stop us downloading the images over and over but we can fix that with something like SDWebImage right?

Sadly this is what the majority of projects will do, but this is not the way to do things.

Why is this bad?

The problem lies in the lifecycle of a UITableViewCell. You have no way of knowing for sure how long a cell will exist for. As soon as it's scrolled off the page it will be marked for recycling and potentially reused for the cell that is now coming on screen. This makes our code inconsistent and we want to avoid that at all costs!

This issue becomes even worse when you add things like really fast user scrolling, more expensive async operations or a combination of the two!

Best case scenario is nothing goes wrong, but you're probably wasting a lot of resources running async operations over and over that may or may not get a chance to complete and cache. Worst case is you start getting random EXC_BAD_ACCESS errors that you will tear your hair out trying to debug.

For these reasons, much like our view controller, we should be aiming to make our cells as dumb as possible. They should be a simple view that takes simple values.

Great!... but some of my data doesn't live on my device and I NEED to grab it asynchronously.

Not a problem, I am going to show you how you can introduce consistency and predictability back into your code by managing async operations in a way that decouples them from the lifecycle of your UITableViewCell allowing them to complete, even if your cell gets recycled. This way, with some simple caching, the data will be ready for the cell next time it's shown giving that buttery smooth interface we all love to use!

A better way

We are going to introduce two new classes to help us manage these operations. Lets take a look!

TableOperation

This class is a simple wrapper around an async operation.

typedef void(^tableOperationSuccessBlock)(id value);  
typedef void(^tableOperationFailureBlock)(NSError *error);  
typedef void(^tableOperationResultBlock)(tableOperationSuccessBlock success, tableOperationFailureBlock failure);

@interface TableOperation : NSObject
-(instancetype)initWithOperation:(tableOperationResultBlock)result;
-(void)execute;
@property (readonly) id value;
@end

We will pass in our async operation via the constructor. Calling execute will run the block on a background thread. Upon successful completion of the operation the result will be cached in value, once this happens any more calls to execute will do nothing as we already have the value.

A new concept for some here might be the tableOperationResultBlock block. This block in turn provides two blocks as its parameters that can be called depending on the success or failure of the operation. Don't worry if you don't understand just yet, we will see it in practice soon.

TableOperationsManager

This class manages the collection of TableOperation objects.

@class TableOperation;

typedef void(^didGetDataForCellBlock)(NSIndexPath *indexPath, id value);

@interface TableOperationsManager : NSObject
-(void)getDataForCellAtIndexPath:(NSIndexPath *)indexPath;
-(void)addOperationIfNeeded:(TableOperation *)operation indexPath:(NSIndexPath *)indexPath;
-(void)invalidate;
@property (nonatomic, copy) didGetDataForCellBlock didGetDataForCell;
@end

TableOperation objects are passed to addOperationIfNeeded:indexPath and if no operation exists for the given indexPath they will be stored. The collection of operations can be cleaned up using invalidate.

As our datasource/delegate builds and shows cells it can make calls to getDataForCellAtIndexPath: to execute operations if needed, the results will then be delivered via the didGetDataForCell block on the main thread.

The implementations for these two classes are very simple and can be found here. Go grab them and add them to our project before continuing.

Freeing our cells from their burden

To use our new classes we will first create a category on our VCTableCellData class. It will give us a TableOperation for that specific VCTableCellData object.

//VCTableCellData+TableOperation.h
#import <UIKit/UIKit.h>
@class TableOperation;

@interface VCTableCellData (TableOperation)
-(TableOperation *)operation;
@end


//VCTableCellData+TableOperation.m
#import "TableOperation.h"

@implementation VCTableCellData (TableOperation)
-(TableOperation *)operation {
    return [[TableOperation alloc] initWithOperation:^(tableOperationSuccessBlock success, tableOperationFailureBlock failure) {
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:self.url]];
        if (data == nil) {
            failure([self errorWithDescription:@"No data returned"]);

        } else {
            UIImage *image = [UIImage imageWithData:data];
            if (image == nil) { failure([self errorWithDescription:@"Unable to construct image"]); }
            else { success(image); }
        }
    }];
}

-(NSError *)errorWithDescription:(NSString *)message {
    return [NSError errorWithDomain:NSStringFromClass([self class]) code:0
                           userInfo:@{NSLocalizedDescriptionKey: message}];
}
@end

This operation will perform the image download for a row in our table on a background thread. It then calls either the success block along with the image or failure with an NSError. This is the technique described earlier which, by passing through blocks as parameters, allows the flow of code to continue on the path we need it to.

Next we will update our cells so they don't need to worry about async stuff anymore. Add the following method to the header of TableCell

-(void)updateImage:(UIImage *)image;

then update the implementation

@implementation TableCell
-(void)setup:(VCTableCellData *)data {
    self.cellTitle.text = [NSString stringWithFormat:@"Entry ID: %@", data.id];
    self.cellImage.image = nil;
}
-(void)updateImage:(UIImage *)image {
    self.cellImage.image = image;
}
@end

Our cells are free! Now they only need to worry about showing simple data when we decide they should.

Putting the code where it should be

With all the pieces of the puzzle ready to go it's time to put it together... First we give our table related objects a public property for our TableOperationsManager so add

@class TableOperationsManager;
@property (nonatomic, strong) TableOperationsManager *manager;

to VCTableDataSource, VCTableDelegate and VCTableCoordinator.

VCTableCoordinator

Add the following code to VCTableCoordinator.m

#import "TableOperationsManager.h"

-(void)bindToTableOperationsManager {
    self.dataSource.manager = self.manager;
    self.delegate.manager = self.manager;
    self.manager.didGetDataForCell = [self tableOperationsManagerDidGetDataForCell];
}
-(didGetDataForCellBlock)tableOperationsManagerDidGetDataForCell {
    return ^(NSIndexPath *indexPath, UIImage *image) {
        TableCell *cell = (TableCell *)[self.tableView cellForRowAtIndexPath:indexPath];
        if (cell) { [cell updateImage:image]; }
    };
}

Finally add

self.manager = [TableOperationsManager new];  

to awakeFromNib and

[self bindToTableOperationsManager];
[self.manager invalidate];

to the start of reloadData:.

This code will ensure our VCTableCoordinator is now notified when our async operations complete. It will then look for the related UITableViewCell and, if it is visible, pass the image through so it can be displayed.

VCTableDataSource

Add the following imports to VCTableDataSource.m

#import "VCTableCellData+TableOperation.h"
#import "TableOperationsManager.h"

and update tableView:cellForRowAtIndexPath: to the following:

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([TableCell class]) forIndexPath:indexPath];

    VCTableCellData *data = self.data[indexPath.row];
    TableOperation *operation = [data operation];
    [self.manager addOperationIfNeeded:operation indexPath:indexPath];
    [self.manager getDataForCellAtIndexPath:indexPath];

    return cell;
}

This will ensure that, only when needed, each cell's operation is added to our TableOperationsManager and that it is executed.

That's Awesynchronous!

Go ahead and edit the title method in VCTableModel to make it say "Async Table View Example" to let the other view controllers know that this one is better! ;)

NOTE: It's important to note that the design shown in this article doesn't only apply to the table view. I highly recommend something like this for collection views and any other container style views whose 'cells' have an undetermined lifespan

Fire up the project and enjoy the buttery goodness! You can find the full source here

That's it for this series! I hope you've enjoyed it and hopefully learnt something new!