Do interface builder and storyboards drive you crazy? Do you like the idea of laying out out your views once, such that they get placed correctly on the ipad, iphone4, and iphone5, in both landscape and portrait modes, adjusting automatically for the height of tab and navigation bars? I think a pitfall for new iOS developers is that tutorials teach only interface builder and lot of people are never made aware that there could be an alternative. Well, there is an alternative. Keep an open mind and hear me out…

What’s wrong with the status quo?

Here are a few reasons why you might want to split up with interface builder (IB). You may not have had all these problems, but i’m sure a few of these will ring true.

You may also want to skip over this section if you don’t need any more convincing that IB isn’t fantastic.

  • Every time you open a xib file in IB, xcode rearranges the file for no good reason. Now, most people like to keep their git history clean - but if every commit has some irrelevant xib changes, it’s not ideal.
  • You can’t localize strings in a xib without creating a whole new xib file.
  • IB gets you half-way there: You inevitably end up needing a styling, layout, or logic setting that you’ll need to drop down into code for. This means your UI/logic gets spread across both your code and your xib’s.
  • You can only do really simple layout logic. This was less of an issue before the iPhone 5 came out, but now your layout has to be fully flexible to handle different heights. Autoresiing flags are very basic, or you can use the constraints layout, but i’ve yet to meet anyone who’s had much success with that.
  • You just can’t merge xib files (most of the time). This issue becomes even worse when you’re using a single storyboard file, where conflicts become even more likely. I’ll admit that if you’re a solo developer, this won’t worry you.
  • Developing with storyboards just isn’t practical unless you’ve got a 27” iMac. We don’t all have that luxury.
  • It’s really hard to get pixel-perfect design using IB.
  • It’s really hard to make rotated layouts work properly.
  • This one’s a bit more subjective: I really like code, it’s what i enjoy writing, and IB just feels clumsy to me.

A better way

Here’s what I’ve used at my last four contracts, making properly complicated apps. It may look like a bit more work on the surface, but I can attest that it has well and truly paid off. Alright here goes:

View Controllers

In my apps, for each ViewController I make a ‘XXXViewController’ class. This class looks like so:

#import "MyViewController.h"

#import "MyView.h"

@implementation AreaViewController {
    MyView *_myView;
}

- (id)init {
    if (self = [super init]) {
	    // Do init-y stuff here...
    }
    return self;
}

- (void)loadView {
    _myView = [[MyView alloc] init];
    // Configure _myView...
    self.view = _areaView;
}
...

The gist of the above is that by creating a MyView and setting it to self.view, it becomes the managed view of this view controller. We don’t even need to set its frame - that is taken care for us by the parent VC (typically a UINavigationController in most apps). It’s all quite neat.

Notice we’re just using ‘init’, instead of initWithNibName, too. This keeps things cleaner when actually instantiating one of these view controllers.

Views

The corresponding MyView which is managed by the view controller looks like below:

#import "MyView.h"

#import "UIView+Helpers.h"

static int kLabelMargin = 10;
static int kLabelHeight = 44;

@implementation AreaView {
    UILabel *_someLabel;
    UILabel *_bottomLabel;
}

- (id)init {
    if (self = [super init]) {
	    _someLabel = [self addLabelWithText:@"Hello"];
	    _bottomLabel = [self addLabelWithText:@"Goodbye"];
	    … create other subviews
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    int width = self.bounds.size.width;
    int height = self.bounds.size.height;
    
    _someLabel.frame = CGRectMake(kLabelMargin, kLabelMargin,
	    width - 2*kLabelMargin, kLabelHeight);
    _bottomLabel.frame = CGRectMake(kLabelMargin,
	    height - kLabelMargin - kLabelHeight,
	    width - 2*kLabelMargin, kLabelHeight);
}

And with the above code, you’ll have two labels centred at the top and bottom of the screen respectively, with 10px margins. And even if you rotate the device to another orientation, it’ll correctly position them again at the top and bottom, with the appropriate width. This code will work for the iPad, iPhone4, and iPhone5 unchanged. And look, no magic numbers for the various screen sizes.

Also, if you position it inside a navigation controller or tab controller (or both), it’ll still get the layout correct, effortlessly, without you needing to account for the height of the navigation/tab bars.

Helpers

But where did addLabelWithText come from? To make the init methods of custom views manageable, it’s best to make a category on UIView with some helper methods to create labels / image views / buttons / etc. The rule of thumb I follow is that these helpers must:

  • Create the new view
  • Apply common styles (eg font size, colour, etc)
  • Add the view as a subview of self
  • Return the new view

These methods are pretty simple to create, so i’ll only show one as an example:

- (UIButton *)addButtonWithImageNamed:(NSString *)name {
    UIImage *image = [UIImage imageNamed:name];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setImage:image forState:UIControlStateNormal];
    button.frame = CGRectMake(0, 0,
	    image.size.width, image.size.height);
    [self addSubview:button];
    return button;
}

Using it

So, to use the new view controller, we’d have something like below in the app delegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
    // Override point for customization after application launch…
    	
    UIViewController *myVC = [[MyViewController alloc] init];
    self.viewController = [UINavigationController alloc]
	    initWithRootViewController:myVC];
	    
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    return YES;
}

Notice that it’s a simple matter of calling alloc init, without requiring the bulky initWithNibName.

Closing thoughts

So here are some closing thoughts i’ve had for this technique.

It gives a clearer understanding of what’s actually going on and the structure of your VC hierarchy. I just feel more ‘in control’ coding this way. It is a bit slower, but I feel like it makes up for that in spending less time scratching your head over quirky layout issues etc.

Another neat trick: if you have subviews that you only want to be able to see on the iPhone 5, but hide them on the iPhone 4 because their positioning clashes with another view and they’re not critically important, you can do something like the following at the bottom of your layoutSubviews method:

_optionalView.hidden = CGRectIntersectsRect(_optionalView.frame, _importantView.frame);

Having said all this, if you end up using this technique on your more complicated screens, and stick with xib’s for your simpler screens, I’d agree that’s a reasonable compromise. Maybe just keep this technique in mind for those times that IB really starts getting in the way? Just another tool for your toolbox.

I just feel like using IB for your UI is like back in the early days of web development, when everyone was using various WYSIWYG tools (remember FrontPage, anybody?) to create HTML files, with varying degrees of success. And over the years, it became the ‘done thing’ to do your HTML by hand, in a text editor. I think, for more complicated apps at least, iOS will tend the same way.

Have a nice weekend!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

iOS Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS