View controllers become gargantuan because they’re doing too many things. Keyboard management, user input, data transformation, view allocation — which of these is really the purview of the view controller? Which should be delegated to other objects? In this post, we’ll explore isolating each of these responsiblities into its own object. This will help us sequester bits of complex code, and make our code more readable.

In a view controller, these responsibilities might be grouped into #pragma mark sections. When that happens, it’s usually time to start thinking about breaking it apart into smaller components.

Data Source

The Data Source Pattern is a way of isolating the logic around which objects live behind what index paths. Particularly in complicated table views, it can be useful to remove all of the logic of “Which cells are visible under these conditions?” from your view controller. If you’ve ever written a table view where you’re constantly comparing integers of rows and sections, a data source object is for you.

Data source objects can literally conform to the UITableViewDataSource protocol, but I’ve found that configuring cells with those objects is a different role than managing index paths, so I like to keep these two objects separate.

A simple example of a data source might handle sectioning logic for you.

@implementation SKSectionedDataSource : NSObject

- (instancetype)initWithObjects:(NSArray *)objects sectioningKey:(NSString *)sectioningKey {
    self = [super init];
    if (!self) return nil;

    [self sectionObjects:objects withKey:sectioningKey];

    return self;
}

- (void)sectionObjects:(NSArray *)objects withKey:(NSString *)sectioningKey {
    self.sectionedObjects = //section the objects array
}

- (NSUInteger)numberOfSections {
    return self.sectionedObjects.count;
}

- (NSUInteger)numberOfObjectsInSection:(NSUInteger)section {
    return [self.sectionedObjects[section] count];
}

- (id)objectAtIndexPath:(NSIndexPath *)indexPath {
    return self.sectionedObjects[indexPath.section][indexPath.row];
}

@end

While this data source is designed to be abstract and reusable, don’t be afraid to make a data source that’s used in only one place in your app. Separating index path management logic from your view controller is a noble goal in itself. Especially for highly dynamic table views, it’s good to have an object that notifies the view controller with messages like “Hey, I have a new object at this index path”; the view controller can then pass that along to the table view in the form of an animation.

This pattern can also encapsulate your retrieval logic. A remote data source can fetch a collection of objects from an API. UIViewController is a UI object, and this is a great way to get networking code out of your view controller.

If the interface to all your data sources is stable (with a protocol for example), you can write a special data source that is composed of an arbitrary number of other data sources. Each sub-data-source becomes its own section in the multi data source. Using this logic to combine data sources is a great way to avoid having to write dreadful index comparison code. The data source will manage it all for you!

Standard Composition

View controllers can be composed using the View Controller Containment APIs introduced in iOS 5. If your view controller is composed of several logical units that could each be their own view controller, consider using Composition to break them apart. A practical application of this litmus test is a screen with multiple table views or collection views.

On a screen with a header and a grid view, we could lazy load our two view controllers, and lay them out properly when prompted by the system.

- (SKHeaderViewController *)headerViewController {
    if (!_headerViewController) {
        SKHeaderViewController *headerViewController = [[SKHeaderViewController alloc] init];

        [self addChildViewController:headerViewController];
        [headerViewController didMoveToParentViewController:self];

        [self.view addSubview:headerViewController.view];

        self.headerViewController = headerViewController;
    }
    return _headerViewController;
}

- (SKGridViewController *)gridViewController {
    if (!_gridViewController) {
        SKGridViewController *gridViewController = [[SKGridViewController alloc] init];

        [self addChildViewController:gridViewController];
        [gridViewController didMoveToParentViewController:self];

        [self.view addSubview:gridViewController.view];

        self.gridViewController = gridViewController;
    }
    return _gridViewController;
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    CGRect workingRect = self.view.bounds;

    CGRect headerRect = CGRectZero, gridRect = CGRectZero;
    CGRectDivide(workingRect, &headerRect, &gridRect, 44, CGRectMinYEdge);

    self.headerViewController.view.frame = tagHeaderRect;
    self.gridViewController.view.frame = gridRect;
}

The resulting sub-view controllers, each with their own collection view, handle one uniform type of data. They are much smaller and easier to understand and change because of it.

Smarter Views

If you’re allocating all of your view controller’s subviews inside of the view controller’s class, you may consider using a Smarter View. UIViewController defaults to using UIView for its view property, but you can override it with your own view. You can use -loadView as the access point for this, as long as you set self.view in that method.

@implementation SKProfileViewController

- (void)loadView {
    self.view = [SKProfileView new];
}

//...

@end

@implementation SKProfileView : NSObject

- (UILabel *)nameLabel {
    if (!_nameLabel) {
        UILabel *nameLabel = [UILabel new];
        //configure font, color, etc
        [self addSubview:nameLabel];
        self.nameLabel = nameLabel;
    }
    return _nameLabel;
}

- (UIImageView *)avatarImageView {
    if (!_avatarImageView) {
        UIImageView * avatarImageView = [UIImageView new];
        [self addSubview:avatarImageView];
        self.avatarImageView = avatarImageView;
    }
    return _avatarImageView
}

- (void)layoutSubviews {
    //perform layout
}

@end

You can simply redeclare @property (nonatomic) SKProfileView *view, and because it is a more specific type than UIView, the analyzer will do the right thing and assume self.view is an SKProfileView. This is called a covariant return type and is very useful for this pattern. (The compiler needs to know that your class is a subclass of UIView, so make sure you import the .h file instead of doing an @class forward declaration! Update: In Xcode 6.3, you also have to declare the property as dynamic, by including the code @dynamic view; in your implementation block.)

Presenter

The Presenter Pattern wraps a model object, transforms its properties for display, and exposes messages for those transformed properties. It is also known in other contexts as Presentation Model, the Exhibit pattern, and ViewModel.

@implementation SKUserPresenter : NSObject

- (instancetype)initWithUser:(SKUser *)user {
    self = [super init];
    if (!self) return nil;
    _user = user;
    return self;
}

- (NSString *)name {
    return self.user.name;
}

- (NSString *)followerCountString {
    if (self.user.followerCount == 0) {
        return @"";
    }
    return [NSString stringWithFormat:@"%@ followers", [NSNumberFormatter localizedStringFromNumber:@(_user.followerCount) numberStyle:NSNumberFormatterDecimalStyle]];
}

- (NSString *)followersString {
    NSMutableString *followersString = [@"Followed by " mutableCopy];
    [followersString appendString:[self.class.arrayFormatter stringFromArray:[self.user.topFollowers valueForKey:@"name"]];
    return followersString;
}

+ (TTTArrayFormatter*) arrayFormatter {
    static TTTArrayFormatter *_arrayFormatter;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _arrayFormatter = [[TTTArrayFormatter alloc] init];
        _arrayFormatter.usesAbbreviatedConjunction = YES;
    });
    return _arrayFormatter;
}

@end

Crucially, the model object itself is not exposed. The Presenter serves as the gatekeeper to the model. This ensures that the view controller can’t sidestep the presenter and directly access the model. Architecture like this limits your dependency graph, and since the SKUser model touches fewer classes, changing it will causes fewer effects in your app.

Binding pattern

In method form, this might be called -configureView. The Binding Pattern updates a view with model data as it changes. Cocoa is a natural place to use this because KVO can observe the model, and KVC can read from the model and “write” to the view. Cocoa Bindings are the AppKit version of this pattern. Third-party libraries like Reactive Cocoa are also effective for this pattern, but may be overkill.

This pattern works really well in conjunction with the Presenter Pattern, using one object to transform values, and another to apply them to your view.

@implementation SKProfileBinding : NSObject

- (instancetype)initWithView:(SKProfileView *)view presenter:(SKUserPresenter *)presenter {
    self = [super init];
    if (!self) return nil;
    _view = view;
    _presenter = presenter;
    return self;
}

- (NSDictionary *)bindings {
    return @{
              @"name": @"nameLabel.text",
              @"followerCountString": @"followerCountLabel.text",
            };
}

- (void)updateView {
    [self.bindings enumerateKeysAndObjectsUsingBlock:^(id presenterKeyPath, id viewKeyPath, BOOL *stop) {
        id newValue = [self.presenter valueForKeyPath:presenterKeyPath];
        [self.view setObject:newvalue forKeyPath:viewKeyPath];
    }];
}

@end

(Note that our simple presenter from above isn’t necessarily KVO-able, but it could be made to be so.)

As with all of these patterns, you don’t have to figure out the perfect abstraction on your first attempt. Don’t be afraid to make an object that is only usable in one specific case. The goal is not to eliminate code reuse, it’s to simplify our classes so that they’re easier to maneuver through and understand.

Interaction pattern

The ease of typing actionSheet.delegate = self is part of the reason that view controllers become too big to fail. In Smaltalk, the entire role of the Controller object was for accepting user input and updating the views and models. Interactions today are more complex, and they cause bulky code in the view controller.

Interactions often include an initial user input (like a button press), optional additional user input (“Are you sure you want to X?”), and then some activity, like a network request or state change. The entire lifecycle of that operation can be wrapped up inside the Interaction Object. The example below creates the interaction object when the button is tapped, but adding the Interaction Object as the target of an action, like [button addTarget:self.followUserInteraction action:@selector(follow)] is also good.

@implementation SKProfileViewController

- (void)followButtonTapped:(id)sender {
    self.followUserInteraction = [[SKFollowUserInteraction alloc] initWithUserToFollow:self.user delegate:self];
    [self.followUserInteraction follow];
}

- (void)interactionCompleted:(SKFollowUserInteraction *)interaction {
    [self.binding updateView];
}

//...

@end

@implementation SKFollowUserInteraction : NSObject <UIAlertViewDelegate>

- (instancetype)initWithUserToFollow:user delegate:(id<InteractionDelegate>)delegate {
    self = [super init];
    if !(self) return nil;
    _user = user;
    _delegate = delegate;
    return self;
}

- (void)follow {
    [[[UIAlertView alloc] initWithTitle:nil
                                message:@"Are you sure you want to follow this user?"
                               delegate:self
                      cancelButtonTitle:@"Cancel"
                      otherButtonTitles:@"Follow", nil] show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if  ([alertView buttonTitleAtIndex:buttonIndex] isEqual:@"Follow"]) {
        [self.user.APIGateway followWithCompletionBlock:^{
            [self.delegate interactionCompleted:self];
        }];
    }
}

@end

The old style of alert view and action sheet delegates make this pattern more apparent, but it works just as well with the new iOS 8 UIAlertController APIs.

Keyboard Manager

Updating the view after the keyboard state changes is another concern that is classically stuck in the view controller, but this responsibility can easily be shifted in a Keyboard Manager. There are implementations of this pattern designed to work in every case, but again, if that’s overkill, don’t be afraid to write your own quick version of this.

@implementation SKNewPostKeyboardManager : NSObject

- (instancetype)initWithTableView:(UITableView *)tableView {
    self = [super init];
    if (!self) return nil;
    _tableView = tableView;
    return self;
}

- (void)beginObservingKeyboard {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
}

- (void)endObservingKeyboard {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidHideNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
}

- (void)keyboardWillShow:(NSNotification *)note {
    CGRect keyboardRect = [[note.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

    UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0f, CGRectGetHeight(keyboardRect), 0.0f);
    self.tableView.contentInset = contentInsets;
    self.tableView.scrollIndicatorInsets = contentInsets;
}

- (void)keyboardDidHide:(NSNotification *)note {
    UIEdgeInsets contentInset = UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0f, self.oldBottomContentInset, 0.0f);
    self.tableView.contentInset = contentInset;
    self.tableView.scrollIndicatorInsets = contentInset;
}

@end

You can call -beginObservingKeyboard and -endObservingKeyboard from -viewDidAppear and -viewWillDisappear or wherever’s appropriate.

Navigating from screen to screen is normally done with a call to -pushViewController:animated:. As these transitions get more complicated, you can delegate this task to a Navigator object. Especially in a universal iPhone/iPad app, navigation needs to change depending on what size class your app is currently running in.

@protocol SKUserNavigator <NSObject>

- (void)navigateToFollowersForUser:(SKUser *)user;

@end

@implementation SKiPhoneUserNavigator : NSObject<SKUserNavigator>

- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {
    self = [super init];
    if (!self) return nil;
    _navigationController = navigationController;
    return self;
}

- (void)navigateToFollowersForUser:(SKUser *)user {
    SKFollowerListViewController *followerList = [[SKFollowerListViewController alloc] initWithUser:user];
    [self.navigationController pushViewController:followerList animated:YES];
}

@end

@implementation SKiPadUserNavigator : NSObject<SKUserNavigator>

- (instancetype)initWithUserViewController:(SKUserViewController *)userViewController {
    self = [super init];
    if (!self) return nil;
    _userViewController = userViewController;
    return self;
}

- (void)navigateToFollowersForUser:(SKUser *)user {
    SKFollowerListViewController *followerList = [[SKFollowerListViewController alloc] initWithUser:user];
    self.userViewController.supplementalViewController = followerList;
}

This highlights one of the benefits to using lots of small objects instead of one big object. They can be changed, rewritten, and replaced very quickly. Instead of shameful conditional code all over your view controller, you can just set self.navigator to [SKiPadUserNavigator new] when on the iPad, and it will respond to the same -navigateToFollowersForUser: method. Tell, don’t ask!

Wrapping up

Historically, Apple’s SDKs only contain the bare minimum of components, and those APIs push you towards Massive View Controller. By tracking down the responsibilities of your view controllers, separating the abstractions out, and creating true single-responsibility objects, we can begin to reign those gnarly classes in and make them managable again.