Frame geometry macros to improve your UIKit code
I find myself doing more and more dynamic UI layout in iOS apps these days. When elements of a UI need to resize to fit their contents, or move to accommodate other elements, the layout code can get complex and verbose. We’ve developed a set of Objective-C UIKit macros that help make this code more readable, self-documenting, and easy to change.
When a view is offset from another view, you might write something like this:
These macros help make that code more concise and scannable:
You can download the macros from our TBMacros Github repo. First let’s take a look at an example
This view is designed to accommodate a range of variable content, using the following layout rules:
- The thumbnail image is variable size.
- The headline should always be top-aligned with the thumbnail, 5 points from the thumbnail on the left, and 10 points from the right edge.
- The headline should resize vertically to fit its contents.
- The byline should be left-aligned with the headline, and 5 points below the headline.
- The date should be 5 points to the right of the byline.
- There should be 10 points of padding all around the outside.
- The whole view should resize vertically to fit its contents.
The macros we’ll use are the following:
- LEFT gets the frame’s origin.x
- RIGHT gets the frames’s origin.x plus its width
- TOP gets the frame’s origin.y
- BOTTOM gets the frame’s origin.y plus its height
You can think of them as describing a view’s coordinates relative to the superview’s frame.
Now here’s the code to lay out our view.
CGFloat outsidePadding = 10;
CGFloat paddingBetweenElements = 5;
CGFloat maxHeadlineHeight = 200;
// resize the thumbnail
self.thumbnail.frame = CGRectMake(outsidePadding, outsidePadding, self.thumbnail.image.size.width, self.thumbnail.image.size.height};
// resize and position the headline
CGFloat headlineWidth = WIDTH(self) - (RIGHT(self.thumbnail) + paddingBetweenElements + outsidePadding);
CGFloat headlineHeight = [self.headline.text sizeWithFont:self.headline.font
constrainedToSize:CGSizeMake(headlineWidth, maxHeadlineHeight)].height;
self.headline.frame = CGRectMake(RIGHT(self.thumbnail) + paddingBetweenElements, TOP(self.thumbnail), headlineWidth, headlineHeight);
// resize and position the byline
CGFloat bylineWidth = [self.byline.text sizeWithFont:self.byline.font].width;
CGFloat bylineHeight = self.byline.font.lineHeight;
self.byline.frame = CGRectMake(LEFT(self.headline), BOTTOM(self.headline) + paddingBetweenElements, bylineWidth, bylineHeight);
// resize and position the date
CGFloat dateWidth = [self.date.text sizeWithFont:self.date.font].width;
self.date.frame = CGRectMake(RIGHT(self.byline) + paddingBetweenElements, TOP(self.byline), dateWidth, bylineHeight);
// resize the whole view
CGFloat bottom = CGFloat bottom = fmaxf(BOTTOM(self.thumbnail), BOTTOM(self.byline));
// find this and other useful UIView categories at https://github.com/twobitlabs/TBLCategories
[self setHeight:bottom + outsidePadding];
}
Note that there are very few fixed values in this code. Everything is based on the dimensions and placement of another element. This has a few useful effects:
- The layout is very adaptive–any of the content can change and the view will lay itself out correctly.
- The layout code describes a set of rules that went into the design of this view–how elements relate to each other and fill the available space. Someone can come along later and understand the intent behind the code. And when there’s a layout bug to fix, it’s easier to understand the cause.
- It’s easy to change the design rules. Designer says there’s too much padding? Done. Elements crammed in too tight? Fixed. Byline font unreadably small? No problem.
But isn’t this what autolayout is for?
Yes, this is exactly what autolayout was designed for. Unfortunately, autolayout has some limitations:
- It’s only available on iOS 6 and above.
- While it’s great that Interface Builder creates constraints dynamically as you drag and drop, it can be maddeningly difficult to determine which automatic constraints are having a particular effect on your view, so modifying them can be time consuming and frustrating.
- There are some dynamic layouts that autolayout just isn’t optimized for. Once you figure out how to express a layout rule in autolayout’s language, that constraint may not read like a relationship in the same simple way the code above does.
Don’t get me wrong–autolayout is awesome. What Apple has done with creating a markup language that maps to layout rules is nothing short of amazing. And it’s really great for layouts that aren’t inherently dynamic, but need to adapt to localization differences like longer label text.
But in my limited experience with autolayout, I’ve found that it’s just not as expressive or adaptable for layouts that have to adapt to really variable content.
4 Comments
nik heger
March 26, 2014Heh. I made a UIView category that does the same thing, example
UIView+x
-(void) x_width {
return self.frame.size.width;
}
-(void) x_right {
return self.frame.size.width + self.frame.origin.y;
}
// plus setters …
then in code
[view2 x_setX:view1.x_right + 10];
[view3 x_setWidth:view1.x_width];
…. and so on. Makes UX code much less painful.
Christopher Pickslay
March 27, 2014Cool. Thanks for sharing!
Richard
April 10, 2014You’ll be pleased to know that this (and more) is already provided in CoreGraphics: CGRectGetMinX, CGRectGetMaxY, CGRectInset, CGRectOffset, etc.
https://developer.apple.com/library/mac/documentation/graphicsimaging/reference/CGGeometry/Reference/reference.html
Christopher Pickslay
April 10, 2014Yes, I’m familiar with those, though I find RIGHT, LEFT, BOTTOM, etc to be much more scannable.