View Layout Code Doesn't Have to Suck So Much

| Comments

One of my favorite parts about being an iOS developer is the opportunity to create amazing User Interfaces. I love nothing more than taking an awesome designer’s PSD and actualizing it on the iPhone or iPad. One my least favorite parts about being an iOS developer is…programmatic view configuration. It’s painful, it’s tedious, it isn’t always immediate obvious what’s happening, and it just bloats class files with a lot of extra cruft. In general, I dislike lots of boilerplate code, but this is especially true about setting up a complex view hierarchy entirely in code. So how do I minimize the inevitable impact of configuring views programmatically?

Use Interface Builder

Yes, this one should be a no-brainer, but it’s still worth mentioning. I occasionally will hear a “hardcore” developer make statements like, “No way, man, you have to do it all programmatically. Interface Builder is a joke.” Yeah, well I’m a visual person, and that “joke” lets me complete 90% of my view configuration easily and accurately. Admittedly, if you’re new to the platform, Interface Builder can take a little while to grasp, especially with all of the behind-the-scenes “magic” that goes on. Once in a blue moon, I’ll still get caught forgetting to wire up an outlet, but for the most part, I love Interface Builder. And with iOS 5, even cooler features are coming that I’m super excited about.

Anyway, let’s do a quick comparison. Which of these is more immediately understandable?

Setting Up a View Hierarchy Programmatically
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
- (void)viewDidLoad
{
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];

    // Let the "fun" begin

    UILabel *awesomenessLabel = [[UILabel alloc] initWithFrame:CGRectMake(20.0f, 20.0f, 111.0f, 21.0f)];
    awesomenessLabel.text = @"Awesomeness";
    awesomenessLabel.textColor = [UIColor whiteColor];
    awesomenessLabel.backgroundColor = [UIColor clearColor];
    awesomenessLabel.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin);

    UISwitch *awesomenessSwitch = [[UISwitch alloc] initWithFrame:CGRectMake(221.0f, 20.0f, 0.0f, 0.0f)];
    awesomenessSwitch.autoresizingMask = (UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin);

    UILabel *levelOfAwesomenessLabel = [[UILabel alloc] initWithFrame:CGRectMake(20.0f, 64.0f, 175.0f, 21.0f)];
    levelOfAwesomenessLabel.text = @"Level of Awesomeness";
    levelOfAwesomenessLabel.textColor = [UIColor whiteColor];
    levelOfAwesomenessLabel.backgroundColor = [UIColor clearColor];
    levelOfAwesomenessLabel.autoresizingMask = awesomenessLabel.autoresizingMask;

    UISlider *levelOfAwesomenessSlider = [[UISlider alloc] initWithFrame:CGRectMake(18.0f, 93.0f, 284.0f, 0.0f)];
    levelOfAwesomenessSlider.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin |
        UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleWidth);

    UIImageView *awesomeImageView = [[UIImageView alloc] initWithFrame:CGRectMake(20.0f, 123.0f, 280.0f, 120.0f)];
    awesomeImageView.autoresizingMask = levelOfAwesomenessSlider.autoresizingMask;

    UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
    CGRect activityIndicatorViewFrame = CGRectMake(20.0f, 252.0f, activityIndicatorView.frame.size.width, activityIndicatorView.frame.size.height);
    activityIndicatorView.frame = activityIndicatorViewFrame;
    activityIndicatorView.autoresizingMask = awesomenessLabel.autoresizingMask;

    UILabel *processingAwesomenessLabel = [[UILabel alloc] initWithFrame:CGRectMake(48.0f, 251.0f, 200.0f, 21.0f)];
    processingAwesomenessLabel.text = @"Processing Awesomeness";
    processingAwesomenessLabel.textColor = [UIColor whiteColor];
    processingAwesomenessLabel.backgroundColor = [UIColor clearColor];
    processingAwesomenessLabel.autoresizingMask = awesomenessLabel.autoresizingMask;

    UIProgressView *progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
    CGRect progressViewFrame = CGRectMake(20.0f, 280.0f, 280.0f, progressView.frame.size.height);
    progressView.frame = progressViewFrame;
    progressView.autoresizingMask = levelOfAwesomenessSlider.autoresizingMask;

    [self.view addSubview:awesomenessLabel];
    [self.view addSubview:awesomenessSwitch];
    [self.view addSubview:levelOfAwesomenessLabel];
    [self.view addSubview:levelOfAwesomenessSlider];
    [self.view addSubview:awesomeImageView];
    [self.view addSubview:activityIndicatorView];
    [self.view addSubview:processingAwesomenessLabel];
    [self.view addSubview:progressView];
}

(The correct answer is, “The first one!”) No, really, at the end of the day, it’s a personal preference. I just don’t see why anyone would actually want to go with a fully-programmatic approach. And the fact that Apple continues to improve upon this notion of visually constructing the view hierarchies of your apps by maintaining and improving Interface Builder? Well, it just seems like it’s the obvious choice.

Also, with Interface Builder, you gain the implicit awareness of Apple’s Human Interface Guidelines. Objects snap to margins based on sensible defaults and guides. In addition, Interface Builder—to some people’s annoyance—won’t allow you to do certain things that don’t “make sense” in Apple’s eyes. One example is that you cannot add a subview to a UIImageView. Nothing prevents you from doing so programmatically, but seemingly, Apple is saying, “Stop that. Don’t add a subview to UIImageView. Your design is wrong.”

Make Your Layout Code Semantic

Okay, so unfortunately, it’s impossible for Interface Builder to solve all of our problems (yet). Some things need to be sized and positioned dynamically, so we have to work with the frame or bounds eventually. But why should it be so laborious and painful? Say we want to move a view down by 10 points:

Shifting a View Down: Failure
1
someView.frame.origin.y += 10.0f;

Easy! But wait, that won’t work. The compiler is throwing a hissy fit (rightly so). We’ve gone and accessed someView’s frame property, followed directly by an access to the frame’s origin struct, followed, finally, by the origin’s y value. Then y is incremented by 10.0f. Of course, accessing a struct makes a copy, so all we end up doing is increasing y on some copied struct and immediately discarding the result. frame is never set to anything new. In any case, the compiler refuses to proceed because frame, acting as a getter, is an r-value; i.e., it is meant to used on the right side of an assignment statement, but we put it on the left side. Bummer. Okay, so how about this?

Shifting a View Down: Success
1
2
3
4
CGRect someViewFrame = someView.frame;
someViewFrame.origin.y += 10.0f;

someView.frame = someViewFrame;

Or, if we really don’t like “wasting” lines of code:

Shifting a View Down: Successful One-Liner
1
someView.frame = CGRectMake(someView.frame.origin.x, (someView.frame.origin.y + 10.0f), someView.frame.size.width, someView.frame.size.height);

That’s a valid one-liner, and frame is an l-value this time because the dot syntax in this case invokes the setter. But boy, that line is long enough to make a T-Rex angry. So we saved a couple lines, but it’s still a pain.

Now, let’s say we want to right align a view, A, next to another view, B, with a padding of 10 points in between them. In addition, we want view A to be vertically centered relative to view B. Here’s how that might look:

Right Align and Vertical Center, Act 1
1
2
3
4
5
6
CGFloat viewAFrameX = (viewB.frame.origin.x - viewA.frame.size.width - 10.0f);
CGFloat viewAFrameY = ((viewB.frame.origin.y + (viewB.frame.size.height / 2.0f)) - (viewA.frame.size.height / 2.0f));
CGRect viewAFrame = viewA.frame;
viewAFrame.origin = CGPointMake(viewAFrameX, viewAFrameY);

viewA.frame = viewAFrame;

So this does the job, but if I had only 5 seconds to look at that code, I probably wouldn’t have figured out what the intent was. Commenting the code is no good, because with a task as common as this, 60% of your source code would end up as trivial, annoying comments. 「仕方ない」as they say in Japan, or “It can’t be helped.” Oh, but it can! Part of the problem is that most view layout code is just lots and lots and lots and lots of repetition. (See what I did there?) E.g., We’re writing stuff like view.frame.origin.x or view.frame.size.width all over the place. Let’s get rid of all that nonsense—or at least die trying! We’ll introduce some shorthand properties to help us out:

Right Align and Vertical Center, Act 2
1
2
3
4
5
6
CGFloat viewAFrameX = (viewB.frameX - viewA.frameWidth - 10.0f);
CGFloat viewAFrameY = ((viewB.frameY + (viewB.frameHeight / 2.0f)) - (viewA.frameHeight / 2.0f));
CGRect viewAFrame = viewA.frame;
viewAFrame.origin = CGPointMake(viewAFrameX, viewAFrameY);

viewA.frame = viewAFrame;

That’s a little better. We can, for example, reduce viewA.frame.origin.x to just viewA.frameX. The same goes for all of frame’s structs’ members. Let’s take it a step further:

Right Align and Vertical Center, Act 3
1
2
3
4
CGFloat viewAFrameX = (viewB.frameX - viewA.frameWidth - 10.0f);
CGFloat viewAFrameY = ((viewB.frameY + (viewB.frameHeight / 2.0f)) - (viewA.frameHeight / 2.0f));

viewA.frameOrigin = CGPointMake(viewAFrameX, viewAFrameY);

Even better. But since we’ve gone that far, how about…

Right-Align and Vertical-Center, Act 4
1
2
viewA.frameX = viewAFrameX = (viewB.frameX - viewA.frameWidth - 10.0f);
viewA.frameY = ((viewB.frameY + (viewB.frameHeight / 2.0f)) - (viewA.frameHeight / 2.0f));

With this, we’ve done a pretty good job of consolidating a bunch of procedural boilerplate—storing individual frame values, copying the existing frame, modifying the copy, reassigning the copied frame—into just two lines. It’s readable, sure, but even still, the intent of what we’re trying to do isn’t obvious. For my final trick, I give you:

Right Align and Vertical Center, Grand Finale
1
2
viewA.frameMaxX = (viewB.frameX - 10.0f);
viewA.frameMidY = viewB.frameMidY;

What does this mean? To understand (if you don’t already), it’s important to remember that by working with view geometry in UIKit, we are always working with respect to the origin. This happens to be the upper left corner of the frame. This works fine a lot of the time, but it is inconvenient when we can more easily express the intent of our view layout with respect to a different origin.

Both right alignment and vertical centering are great examples of this. As we see in the “Grand Finale”, it’s easier to simply take the x value of viewB’s frame (which represents the left edge), subtract 10 points from it (per the original problem description), and assign that position to the right edge of viewA’s frame. The property setter will do the calculations to do the actual frame adjustments with respect to the real origin.

Even more dramatic an improvement is the vertical centering. We merely express that we want viewA’s frame to be set with respect to the vertical center. And the value we want to set it to is the vertical center of viewB’s frame. That is to say, “I want the middle of my frame, with respect to y, to be the middle of some other frame, also with respect to y”. Compare this to the manual calculations we originally used, and it should be clear how much simpler semantic view layout can be.

Of course, these semantic UIView properties don’t exist, so we’ll have to create them (among others): UIView Semantic Layout Properties. These will also be available in LTKit, which I’ll blog about soon. (I think I finally found a focus for it!)

The inspiration for these properties mostly came from a series of CGRect methods that return individual values from the struct. For example, CGRectGetMaxX, which gives you the right edge of the CGRect you pass to the function. I thought, “Well, if you can get these values from a rect, why can’t you set them, too?” Applied broadly to CGRect, this didn’t seem too useful or practical. So I decided instead to target UIView’s frame and bounds properties. Also, looking at CALayer’s anchorPoint property, I realized that you can apply transforms about any arbitrary point within the layer. Not really the same concept, but the fact that you can move the transform’s origin point got me thinking in that direction.

It’s notable that there really isn’t anything special about these properties. They’re just simple calculations used for short-handing really long CGRectMake function calls! But a side effect of the naming—which, by the way, is possibly being reconsidered—is that you get to apply a little meaning behind what you’re actually trying to do to the frame or bounds. In any case, I’ll never develop iOS applications without them.

Wrapping Up

So maybe you agree or disagree with my approach to developing UI layouts. As a general rule, the less code in my final project, the better. Possibly a contrarian opinion, sure, but as you gain more code, you also “gain” more chances for Error to show its ugly face. By the way, Error is probably related to this guy:

I wonder how many people get that reference. Either way, you don’t want to press your luck when dealing with errors…

Anyway, by letting finely-tuned tools—such as Interface Builder—do as much of the mundane, boilerplate work for you, you can focus on just creating awesome apps. And even if you’re the type who insists on keeping everything programmatic, that doesn’t mean you can’t be a little more creative with how you define your view interactions. If you feel there’s a legitimately better way, or if you feel like there’s a strong argument for why I’m wrong, please let me know. I love discussing—not to be confused with yelling and arguing—things like this with other developers. I try to be an open book about stuff like this!

Comments