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…
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.
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:
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.
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.
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:
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;
}
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.
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.
(Comp Sci, Hons - UTS)
Software 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