Put your UIViewController on a diet

I hate to say it but our view controllers have gotten fat, and it's our fault! We feed them protocol after protocol, business rule after business rule. They don't know any better and they just gobble them all up...

This will be a 3 part "Tune up your table view" series in which we will take an old fat view controller, slim it down with some refactoring, freshen it up with some MVVM and then make it fly with some better asynchronous operation management!

In part 1 using refactoring and a couple of Interface Builder tricks hopefully I can provide some motivation to start your journey towards getting your view controllers back in fighting shape!

The current state of things...

UITableViews are pretty integral to most iOS apps so I'm going to use it as an example of how to change a 'fat' view controller into a 'slim' one.

Take a second to look over the example project here. Specifically we will target VCTable.

FatTable

In the scheme of things it's a super simple table view, you could probably argue that it's not even that bloated.

But unfortunately in the real world our view controllers are rarely this simple. Sure they might start out that way but then slowly but surely new features get added and as they say... the best laid plans of mice and men often go astray.

So let's see what steps we can take to make this table a little nicer...

Break it down

Let's have a look at what this view controller is currently doing...

  • It's making an API call to get some pictures (complete with some thread juggling)
  • It's registering the required table view cells
  • It's managing the table's datasource
  • It's handling the table delegate methods

Thats a lot of things that have nothing to do with the view! In fact looking through the code it seems like most of it is all delegate methods and business rules :(

Low hanging fruit...

Looking at this code it seems like the API call is going to be the easiest thing to break away first.

-(void)reloadData {
    [self.refresh beginRefreshing];

    //Do our work in the background
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //Request our data from the server
        NSError *err = nil;
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:kURL]];

        //Check the response from the server
        if (data == nil) {
            err = [NSError errorWithDomain:NSStringFromClass([self class]) code:0
                                  userInfo:@{NSLocalizedDescriptionKey: @"No data returned by server"}];
        }

        //Attempt to deserialize the response into JSON
        NSArray *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];

        //Handle the result on the main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            if (err) {
                //Something went wrong either talking with the server 
                //or converting the received data into json
                [self endRefreshing];

                [[[UIAlertView alloc]
                  initWithTitle:@"Oops.."
                  message:[NSString stringWithFormat:@"An Error Occurred\n\n%@", err.localizedDescription]
                  delegate:nil
                  cancelButtonTitle:@"OK"
                  otherButtonTitles:nil]
                 show];

            } else {
                //Array of items received from the server, convert them into our model objects
                NSMutableArray *items = [NSMutableArray array];
                for (NSDictionary *jsonItem in json) {
                    VCTableCellData *item = [[VCTableCellData alloc] initWithJSON:jsonItem];
                    [items addObject:item];
                }

                [self endRefreshing];

                //Store the model objects and reload the table view
                self.data = items;
                [self.tableView reloadData];
            }
        });
    });
}

This is kind of gross... At the moment, apart from being too long, this method mixes API call with logic relating to the refresh control as well as the success or failure of the request. We should be able to break that coupling up and make this more reusable.

//MyAPI.h

typedef void(^myAPIDidError)(NSError *error);  
typedef void(^myAPIDidGetPhotos)(NSArray *json);

@interface MyAPI : NSObject
-(void)getPhotos:(myAPIDidGetPhotos)complete error:(myAPIDidError)error;
@end



//MyAPI.m

static NSString *kURL = @"http://jsonplaceholder.typicode.com/photos";

@implementation MyAPI
-(void)getPhotos:(myAPIDidGetPhotos)complete error:(myAPIDidError)error {
    //Do our work in the background
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSError *err = nil;
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:kURL]];

        //Check the response from the server
        if (data == nil) {
            err = [NSError errorWithDomain:NSStringFromClass([self class]) code:0
                                    userInfo:@{NSLocalizedDescriptionKey: @"No data returned by server"}];
        }

        //Attempt to deserialize the response into JSON
        NSArray *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];

        //Handle the result on the main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            if (err && error) {
                error(err); //Something went wrong
            } else {
                complete(json); //Success!
            }
        });
    });
}
@end

Thats a bit better now we can also use an instance of MyAPI in any view controller that needs it, next we add a private property for MyApi to our view controller

@property (nonatomic, strong) MyAPI *api;

and create an instance in viewDidLoad

self.api = [MyAPI new];  

Finally we replace our reloadData method

-(void)reloadData {
    [self.refresh beginRefreshing];

    [self.api getPhotos:^(NSArray *json) {
        //Convert JSON items into our model objects
        NSMutableArray *items = [NSMutableArray array];
        for (NSDictionary *jsonItem in json) {
            VCTableCellData *item = [[VCTableCellData alloc] initWithJSON:jsonItem];
            [items addObject:item];
        }

        //Store the items and reload the table view
        [self endRefreshing];
        self.data = items;
        [self.tableView reloadData];

    } error:^(NSError *error) {
        [self endRefreshing];

        //Report the error to the user
        [[[UIAlertView alloc]
          initWithTitle:@"Oops.."
          message:[NSString stringWithFormat:@"An Error Occurred\n\n%@", error.localizedDescription]
          delegate:nil
          cancelButtonTitle:@"OK"
          otherButtonTitles:nil]
         show];
    }];
}

and we will come back to this later.

Protocol overload...

Possibly the biggest cause of view controller bloat would have to be the protocols we need to use for table views, text fields, date pickers etc... I'm certainly not bashing the delegate pattern, it's great for letting us control how things work but there's a bad habit of just loading it all into view controllers, we need to stop!

So how can we do it? Well theres a lesser know feature of Interface Builder which allows you to add any object to your nib/storyboard. These objects will be created when your view is, and they will be ready to go when viewDidLoad is called.

Let's see how it works, we are going to create a separate object for both our UITableViewDataSource and UITableViewDelegate.

Create the following:

//VCTableDataSource.h
#import <UIKit/UIKit.h>

@interface VCTableDataSource : NSObject <UITableViewDataSource>
@property (nonatomic, strong) NSArray *data;
@end



//VCTableDataSource.m
#import "TableCell.h"

@implementation VCTableDataSource
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return (self.data == nil ? 0 : 1);
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.data.count;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([TableCell class]) forIndexPath:indexPath];
    return cell;
}
@end
//VCTableDelegate.h
#import <UIKit/UIKit.h>

@interface VCTableDelegate : UIControl <UITableViewDelegate>
@property (nonatomic, strong) NSArray *data;
@property (readonly) id selectedData;
@end



//VCTableDelegate.m
#import "TableCell.h"

@implementation VCTableDelegate
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    id data = self.data[indexPath.row];
    [((TableCell *)cell) setup:data];
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    _selectedData = self.data[indexPath.row];
    [self sendActionsForControlEvents:UIControlEventValueChanged];
}
@end

And in addition to these new objects we will also create another object to coordinate data reloads using these new objects as well as register our cell for us.

//VCTableCoordinator.h
#import <UIKit/UIKit.h>

@interface VCTableCoordinator : NSObject
-(void)reloadData:(NSArray *)data;
@property (nonatomic, strong) IBOutlet UITableView *tableView;
@end



//VCTableCoordinator.m
#import "VCTableDataSource.h"
#import "VCTableDelegate.h"
#import "TableCell.h"

@interface VCTableCoordinator ()
@property (nonatomic, strong) IBOutlet VCTableDataSource *dataSource;
@property (nonatomic, strong) IBOutlet VCTableDelegate *delegate;
@end

@implementation VCTableCoordinator
#pragma mark - Lifecycle
-(void)awakeFromNib {
    [super awakeFromNib];

    [self registerCells];
}

#pragma mark - Public
-(void)reloadData:(NSArray *)data {
    self.dataSource.data = data;
    self.delegate.data = data;

    [self.tableView reloadData];
}

#pragma mark - Private
-(void)registerCells {
    UINib *nib = [UINib nibWithNibName:NSStringFromClass([TableCell class]) bundle:nil];
    [self.tableView registerNib:nib forCellReuseIdentifier:NSStringFromClass([TableCell class])];
}
@end

Back in our view controller we add a private property for our shiny new VCTableCoordinator

@property (nonatomic, strong) IBOutlet VCTableCoordinator *tableViewCoordinator;

And also add an IBAction for when a cell is selected.

#import "VCTableDelegate.h"

-(IBAction)itemPressed:(VCTableDelegate *)sender {
    VCTableCellData *data = sender.selectedData;
    NSLog(@"Selected ID: %@", data.id);
}

Lastly perform the following:

  • in viewDidLoad change Fat to Slim in the title - You've earnt it!!
  • remove #import "TableCell.h"
  • remove static NSString *kURL = @"http://jsonplaceholder.typicode.com/photos";
  • remove the private properties for NSArray *data and UITableView *tableView
  • remove the protocols UITableViewDataSource and UITableViewDelegate from the declaration in the .m
  • remove all the code under the headings UITableViewDataSource and UITableViewDelegate
  • remove the registerCells method and the call in viewDidLoad
  • update setupPullToRefresh, change self.tableView to self.tableViewCoordinator.tableView
  • find these lines in reloadData:
self.data = items;  
[self.tableView reloadData];

change them to:

[self.tableViewCoordinator reloadData:items];

Observant followers will have noticed that there are a lot of items declared with IBOutlet in these new objects, as well as that last IBAction which lets us handle row selection just by hooking it up!

If we go back to our nib we should first disconnect all existing outlet links (except view). Next looking in the list where you would normally add controls from, find Object and add 3 of them.

Click the first one you added then in the identity inspector change its type to VCTableDataSource. Make the second one VCTableDelegate and the third VCTableCoordinator.

Next comes the cool part, hooking everything up! Now, take your time and make the following connections:

  • File's Owner > Table Coordinator
  • Table Coordinator > Table Delegate
  • Table Coordinator > Table Data Source
  • Table Coordinator > Table View
  • Table View > Table Delegate
  • Table View > Table Data Source
  • Table Delegate > File's Owner (itemPressed:)

Phew, that was a lot of connections... but so far we have managed to break away a decent amount of code from our view controller!, nawww look how much weight he's lost!

Lean mean view controller!

Fire it up and take your new slim view controller for a spin!!, it behaves exactly the same way fattie did, tap on a cell and you'll get an awesome console log ;)

If you want the source for the slimmed down view controller, you can find it here.

Our table view still has a way to go... it's a little jumpy when scrolling but don't worry, we will address the performance in part 3 but coming up next, MVVM!