Keeping ViewController files small

If you’re working on a complex iOS app, it’s very easy to create a UIViewController subclass with a huge amount of code. One of the many code guidelines I learned at Google was the simple idea of keeping functions and files small. For example, I would recommend trying to keep the vast majority of your functions around 40 lines or less, and trying to keep all of your files under 1000 lines. This post presents a technique that can help you achieve smaller file size.

Why small source files are good

Here are two principles to code by:

  1. Your code should be simple to users, and
  2. Everyone, including you, is a user.

I could write a whole post about these ideas, but let me just summarize by saying that anyone working on your codebase, including future you, cares a lot about how simple things are. When each individual idea in your codebase is succinct and transparent, things are easy to modify and update, and bugs are harder to create and easier to squash.

File size is not a perfect predictor for code simplicity, but it’s a darn good heuristic.

Use categories on your own classes

Instead of having one header and one implementation (h,m) file for your UIViewController subclass, I suggest splitting the main pieces of functionality out into their own files, as categories of your class.

For example, suppose we’re writing a tetris clone for the iPad. (Don’t actually do that, btw. You might get sued.) Let’s have a primary view controller called TetrisViewController. It would be easy for this file to become huge, so we can split different aspects of gameplay into separate pieces, such as this:

  • TetrisViewController.{h,m}
  • TetrisViewController+Tutorial.{h,m}
  • TetrisViewController+Animations.{h,m}
  • TetrisViewController+Network.{h,m}

Since the class is officially declared in TetrisViewController.h, this is where all the instance variables are declared. Any code specific to tutorials, animations, or network play goes in its respective category file. The goal is to partially encapsulate that functionality in its own file, while logically speaking, those methods will have full access to all the variables of the class. A good rule of thumb is to group methods together when you are likely to be editing them together; you want to minimize the need for file-switching while programming.

Template code

Let me fill in the details of how these files might actually look.

// TetrisViewController+Animations.h

#import "TetrisViewController.h"

@interface TetrisViewController (Animations)

// Public method names start with the category name.
- (void)animationsOnCompletionOfRows:(NSArray *)rows;
- (void)animationsOnLevelDone;

@end
// TetrisViewController+Animations.m

#import "TetrisViewController+Animations.h"

// Declare any private methods.
@interface TetrisViewController (AnimationsPrivate)

- (void)animationsForSingleRow:(int)rowIndex;

@end

@implementation TetrisViewController (Animations)

- (void)animationsOnCompletionOfRows:(NSArray *)rows {
  for (NSNumber *rowNumber in rows) {
    [self animationsForSingleRow:[rowNumber intValue]];
  }
}

- (void)animationsOnLevelDone {
  // Execute level done animation.
}

#pragma mark private methods

- (void)animationsForSingleRow:(int)rowIndex {
  // Execute single-row animation.
}

@end

Within the category implementation, you’re free to call any public method on the original view controller, and any methods (including category-private ones) within the category. Note that you can’t call private methods of the original class from the category, at least not without a compiler warning. In practice, I’ve found that this is not a problem. If the categories are separate enough ideas, the methods you need to call from a category will most often naturally be public methods. In some cases where this was not true, I found it made sense to make a previously-private method public to solve this. The small number of times when I did this have all made sense in retrospect, in the sense that the newly-public methods did not break encapsulation.

The header file of the original class doesn’t need to know anything about the category files. The implementation file needs to import them so you can call them. I find it useful to think of these as sort of hooks into the original class. Ideally, you only add about one line of code to the original class per public category method. In other words, you want your code impact on your main m file to be very small when you add a category like this.

Here’s an example of how your main implementation might look:

// TetrisViewController.m

// Import the main header first, to be sensitive to any uncaught dependencies in the header.
#import "TetrisViewController.h"

// Import the category headers next, again trying to catch any header dependencies.
#import "TetrisViewController+Animations.h"
#import "TetrisViewController+Network.h"
#import "TetrisViewController+Tutorial.h"

// Import other headers, declare private methods, etc.

// Then methods, which might include something like this:
- (void)someRowsCompleted {
  NSArray *rowsDone = [self getRowsDone];
  [self incrementScoreBasedOnRows:rowsDone];
  [self animationsOnCompletionOfRows:rowsDone];
}

One last thing: it’s definitely more work to retroactively pull out categories from a huge view controller, so I highly recommend trying to split out categories as you code, as opposed to after you’ve got huge files. I hope that’s helpful!

6 Comments

  1. Posted May 18, 2011 at 1:08 am | Permalink

    This is definitely a very elegant solution but i found a little problem:
    following your template code if you move the implementation of delegate method, for example UITableViewDelegate / UITableViewDataSource, in a category you get a lot of warning because the compiler say that can’t find the delegate method implementation.
    BTW really good post! Thanks for sharing

  2. Posted May 18, 2011 at 7:22 pm | Permalink

    Thanks, @Luca! You’re right — delegate methods, aka any protocols your class instantiates, are expected to be implemented in the main .m file, not in a category.

    I guess you could do something very nontraditional and make a separate .m file, and then #import that file in the body of the main .m file (not at the top). Technically, you can always just use #imports to separate functionality, but I think the cost is that other programmers looking at the code are likely to be confused by that coding style (putting #imports anywhere expect the top of a file), so I wouldn’t recommend it.

  3. Posted December 18, 2011 at 1:58 pm | Permalink

    That’s pretty clean but the big drawback from my point of view is that we cannot generate properties on categories.

  4. Posted December 30, 2011 at 1:14 pm | Permalink

    Nice post! I also agree that delegate methods should probably be in the main view controller but you can always make those methods really short and have them use helpers in a category. That’s the route I’m going

  5. Posted December 31, 2011 at 2:58 am | Permalink

    Nice article Tyler. I’m fairly new to iOS. I used the same approach before reading your post but was not sure if it is a good idea. Now I’m more convinced, though 🙂

    Regarding Luca Bernardis problem. I had exactly the same issue and found a solution for it. At least the compiler is happy and the runtime code works as expected.

    I moved all logic for the table handling into a category (MyViewController+Table.h/m). Instead of declaring the protocols within the main class definition (MyViewController.h), I declared them within the category:

    @interface MyViewController (Table)

    Works like a charm for me. Hope I could help.

  6. Posted December 31, 2011 at 3:01 am | Permalink

    The code example in my previous post got screwed up, another try:

    @interface MyViewController (Table) <UITableViewDelegate, UITableViewDataSource>

One Trackback

  1. […] Go there! […]

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*