Home > Articles > Mobile Application Development & Programming

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

Auto Layout and Frames

With Auto Layout, view frames play a new, less important role. In an Auto Layout world, frames are now the guys in an opera who stands around holding a spear while the constraints and intrinsic content sizes sing their arias. The focus moves from specific sizes and placement to suggestions and relations.

When you worked with Autosizing, frames said where to place views on the screen and how big those views would be. In Auto Layout, constraints determine view size and placement using a geometric element called an alignment rectangle.

As developers create complex views, they may introduce visual ornamentation such as shadows, exterior highlights, reflections, and engraving lines. As they do, these features usually become attached as subviews or sublayers or integrated into a view’s image. As a consequence, a view’s full extent may grow as items are added.

Unlike frames, a view’s alignment rectangle is limited to a core visual element. Its size remains unaffected as new items join the primary view. Consider Figure 1-13 (left). It depicts a view with an attached shadow and a badge. When laying out this view, you want Auto Layout to focus on aligning just the core element.

Figure 1.13

Figure 1-13. A view’s alignment rectangle (center) refers strictly to the core visual element to be aligned, without embellishments.

The center image shows the view’s alignment rectangle. This rectangle excludes all ornamentation, including the drop shadow and badge. It’s the part of the view you want considered when Auto Layout does its work. Contrast this with the rectangle shown in the right image of Figure 1-13. This version includes all the visual ornamentation, extending the view’s frame beyond the area that should be considered for alignment.

This rectangle encompasses all the view’s visual elements. It includes the shadow and badge. These ornaments could potentially throw off a view’s alignment features (for example, its center, bottom, and right) if they were considered during layout.

By working with alignment rectangles instead of frames, Auto Layout ensures that key information, like a view’s edges and center, are properly considered during layout. In Figure 1-14, the adorned view is perfectly aligned on the background grid. Its badge and shadow are not considered during placement.

Figure 1.14

Figure 1-14. Auto Layout only considers this view’s alignment rectangle when laying it out as centered in its parent. The shadow and badge don’t affect its placement.

Visualizing Alignment Rectangles

Both iOS and OS X enable you to overlay views with their alignment rectangles in your running application. You set a simple launch argument from your app’s scheme. This is UIViewShowAlignmentRects for iOS and NSViewShowAlignmentRects for OS X. Set the argument value to YES and make sure to prefix with a dash, as shown in Figure 1-15.

Figure 1.15

Figure 1-15. Set launch arguments in the scheme editor.

When the app runs, rectangles show over each view. The resulting rectangles are light and can be difficult to see.

Image Alignment Insets

Alignment issues affect the way you handle images. Image art often contains hard-coded embellishments, like highlights, shadows, and so forth. These items take up little memory and run more efficiently than those created by adding layer effects. Because of that, many developers use images enhancements in preference to Quartz 2D effects because of their low overhead.

Figure 1-16 demonstrates the typical problem encountered when using image-based ornamentation with Auto Layout. The left image shows a basic image view, whose art I created in Photoshop. I used a standard drop shadow effect. When added to the image view, the 20-point by 20-point area I left for the shadow throws off the view’s alignment rectangle, causing it to appear slightly too high and left.

Figure 1.16

Figure 1-16. Adjust your images to account for alignment when using Auto Layout. At the left, the image view was created with a raw, unadjusted image. It displays slightly too far left and up, which you can inspect by looking at the points where the circle crosses the background grid. I added lines over the screenshot to emphasize where the centering should have occurred. The right screenshot shows the adjusted image view. It centers exactly onto its parent view.

In its default implementation, the image view has no idea that the image contains ornamental elements. You have to tell it how to adjust its intrinsic content so that the alignment rectangle considers just that core material.

To accommodate the shadow, you load and then rebuild the image. This is a two-step process. First, you load the image as you normally would (for example, with imageNamed:). Then, you call imageWithAlignmentRectInsets: on that image to produce a new version supporting the specified insets:

UIImage *image = [[UIImage imageNamed:@"Shadowed.png"]
    imageWithAlignmentRectInsets:UIEdgeInsetsMake(0, 0, 20, 20)];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];

After specifying the alignment rect insets, the updated version now properly aligns, as you see in Figure 1-16, right. I logged out the pertinent details so that you can compare the view details. Here’s what the view frame looks like (it shows the full 200×200 image size), the intrinsic content size built from the image’s alignment insets (180×180), and the resulting alignment rectangle used to center the image view’s frame:

HelloWorld[53122:c07] Frame: {{70, 162}, {200, 200}}
HelloWorld[53122:c07] Intrinsic Content Size: {180, 180}
HelloWorld[53122:c07] Alignment Rect: {{70, 162}, {180, 180}}

Declaring Alignment Rectangles

Cocoa and Cocoa Touch offer a several additional ways to report alignment geometry. You may implement alignmentRectForFrame:, frameForAlignmentRect:, baselineOffsetFromBottom, and alignmentRectInsets. These methods allow your views to declare and translate alignment rectangles from code.

For the most part, thankfully, you can ignore alignment rectangles and insets. Things just, for the most part, work. The edge cases you encounter usually happen when Auto Layout comes into conflict with transforms (and other circumstances when the actual frame doesn’t match the visual frame as with buttons).

A few notes on these items:

  • alignmentRectForFrame: and frameForAlignmentRect: must always be mathematical inverses of each other.
  • Most custom views only need to override alignmentRectInsets to report content location within their frame.
  • baselineOffsetFromBottom is available only for NSView and refers to the distance between the bottom of a view’s alignment rectangle and the view’s content baseline, such as that used for laying out text. This is important when you want to align views to text baselines and not to the lowest point reached by typography descenders, like j and q.

Here’s some information about alignmentRectForFrame:and frameForAlignmentRect: from the UIView.h documentation:

  • These two methods should be inverses of each other. UIKit will call both as part of layout computation. They may be overridden to provide arbitrary transforms between frame and alignment rect, though the two methods must be inverses of each other. However, the default implementation uses alignmentRectInsets, so just override that if it’s applicable. It’s easier to get right.
  • A view that displayed an image with some ornament would typically override these, because the ornamental part of an image would scale up with the size of the frame. Set the NSUserDefault UIViewShowAlignmentRects to YES to see alignment rects drawn.

NSLayoutConstraint.h on OS X adds the following comment:

  • If you do override these be sure to account for the top of your frame being either minY or maxY depending on the superview’s flippedness.

You’ll see this flippedness adjustment made in Listing 1-1, which is introduced in the next section.

Implementing Alignment Rectangles

Listing 1-1 offers a trivial example of code-based alignment geometry. This OS X app builds a fixed-size view and draws a shadowed rounded rectangle into it. When USE_ALIGNMENT_RECTS is set to 1, its alignmentRectForFrame: and frameForAlignmentRect: methods convert to and from frames and alignment rects. As Figure 1-17 shows, these reporting methods allow the view to display with proper alignment.

Figure 1.17

Figure 1-17. Implementing intrinsic content size and frame/alignment rect conversion methods ensures that your view will align and display correctly (left) rather than be misaligned and clipped (right).

Listing 1-1. Using Code-Based Alignment Frames

@interface CustomView : NSView
@end

@implementation CustomView
- (void) drawRect:(NSRect)dirtyRect
{
    NSBezierPath *path;

    // Calculate offset from frame for 170x170 art
    CGFloat dx = (self.frame.size.width - 170) / 2.0f;
    CGFloat dy = (self.frame.size.height - 170);

    // Draw a shadow
    NSRect rect = NSMakeRect(8 + dx, -8 + dy, 160, 160);
    path = [NSBezierPath
        bezierPathWithRoundedRect:rect xRadius:32 yRadius:32];
    [[[NSColor blackColor] colorWithAlphaComponent:0.3f] set];
    [path fill];

    // Draw fixed-size shape with outline
    rect.origin = CGPointMake(dx, dy);
    path = [NSBezierPath
        bezierPathWithRoundedRect:rect xRadius:32 yRadius:32];
    [[NSColor blackColor] set];
    path.lineWidth = 6;
    [path stroke];
    [ORANGE_COLOR set];
    [path fill];
}

- (NSSize)intrinsicContentSize
{
    // Fixed content size - base + frame
    return NSMakeSize(170, 170);
}

#define USE_ALIGNMENT_RECTS 1
#if USE_ALIGNMENT_RECTS
- (NSRect)frameForAlignmentRect:(NSRect)alignmentRect
{
    // 1 + 10 / 160 = 1.0625
    NSRect rect = (NSRect){.origin = alignmentRect.origin};
    rect.size.width = alignmentRect.size.width * 1.06250;
    rect.size.height = alignmentRect.size.height * 1.06250;
    return rect;
}

- (NSRect)alignmentRectForFrame:(NSRect)frame
{
    // 160 / 170 = 0.94117
    // Account for vertical flippage
    CGFloat dy = (frame.size.height - 170) / 2.0;
    rect.origin = CGPointMake(frame.origin.x, frame.origin.y + dy);

    rect.size.width = frame.size.width * 0.94117;
    rect.size.height = frame.size.height * 0.94117;
    return rect;
}
#endif
@end
  • + Share This
  • 🔖 Save To Your Account