Itty Bitty Labs

Code and technical stuff from Itty Bitty Apps.

The UIView that wouldn't be centered

Recently I was creating a banner for an informational page in one of our iOS apps. The designer had specified three centered lines of information about a property, as a static header above a map and some scrolling information below. As you would think, a perfect case for contained view controllers and constraint-based layout. But what appeared to be the easiest part turned to contain an interesting problem…. The base view controller (“self”) starts by apply some basic constraints to a UIView named bannerView at the top of the screen. (The iOS6 case is not shown here for clarity):

Adding constraints to the banner view
1
2
3
4
5
6
7
  if ([self respondsToSelector:@selector(topLayoutGuide)])
  {
    NSDictionary *viewsToConstrain = @{@"topLayoutGuide" : self.topLayoutGuide, @"bannerView" : self.bannerView};

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[topLayoutGuide][bannerView]" options:0 metrics:nil views:viewsToConstrain]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[bannerView]|" options:0 metrics:nil views:viewsToConstrain]];
  }

The banner view is anchored to the top of the view controller’s view and is constrained to its full width. At this stage bannerView has no frame set and won’t display anything, so it needs to supply its own height constraint. This is achieved by relying on the intrinsic height of its contents – each label is placed in the banner view and centered horizontally by constraint, then bannerView has a constraint applied that stacks each view and at the same time gives the view its intrinsic height, the total of the heights of those views. You could say it really ties the view together. (This is one of the tough concepts in auto layout: constraints don’t apply in one direction, they are all applied and solved simultaneously. Conflicts and ambiguities are the only problem, either the parent or the subview can supply the dimension.) An example:

Centering horizontally and stacking vertically
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
  self.addressLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  // font, color, text etc. are set 
  [self.addressLabel sizeToFit];
  self.addressLabel.translatesAutoresizingMaskIntoConstraints = NO;
  [self addSubview:self.addressLabel];

  // horizontal centering is repeated for the locality label and the features view (not shown)
  [self addConstraint:[NSLayoutConstraint constraintWithItem:self.addressLabel
                                                   attribute:NSLayoutAttributeCenterX
                                                   relatedBy:NSLayoutRelationEqual
                                                      toItem:self
                                                   attribute:NSLayoutAttributeCenterX
                                                  multiplier:1.0f
                                                    constant:0.f]];

  // create and apply the constraint to position everything vertically. All these views have an intrinsic height so bannerView (self) 
  // will have the total height of these views
  NSDictionary *viewsToConstrain = @{@"addressLabel" : self.addressLabel, @"localityLabel" : self.localityLabel, @"featureView" : self.featureView};

  NSString *vFormatString = [NSString stringWithFormat:@"V:|-%f-[addressLabel]-%f-[localityLabel]-%f-[featureView]-%f-|",
                             kAddressLabelTopMargin, kLocalityLabelTopMargin, kFeatureViewTopMargin, kViewBottomMargin];
  [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vFormatString
                                                               options:0
                                                               metrics:nil
                                                                 views:viewsToConstrain]];

The end result is the completed bannerView, anchored to the top of the containing view controller’s view. If the designer decides to alter the font size or order of the labels or the size of featureView then the banner view will be resized through its constraints automatically. In the view controller the next lower view is anchored to the base of the banner view so everything maintains its arrangement. Job done! Or so I thought. A look at the result shows not the three nicely centered elements expected, but two centered elements and one seemingly left aligned on the centre. As the bannerView has been selected, Reveal shows its bounds (in 2d mode) with the blue outline. That’s strange, since the view has the same NSLayoutAttributeCenterX constraint applied as the labels. The featureView is actually a view containing a single label. Once upon a time it was a view containing three icons and three labels, but to improve scrolling efficiency (remove transparency) a font was created containing glyphs for those icons, and the view created using the label’s attributedText property. For a quick sanity check, what if the view is a plain UIView?

Testing with a simple view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  self.featureView = [[UIView alloc] initWithFrame:CGRectZero];
  self.featureView.backgroundColor = [UIColorblueColor];
  [self.featureView addConstraint:[NSLayoutConstraint constraintWithItem:self.featureView
                                                               attribute:NSLayoutAttributeHeight
                                                               relatedBy:NSLayoutRelationEqual
                                                                  toItem:nil
                                                               attribute:NSLayoutAttributeNotAnAttribute
                                                              multiplier:1.0
                                                                constant:28.f]];
  [self.featureView addConstraint:[NSLayoutConstraint constraintWithItem:self.featureView
                                                               attribute:NSLayoutAttributeWidth
                                                               relatedBy:NSLayoutRelationEqual
                                                                  toItem:nil
                                                               attribute:NSLayoutAttributeNotAnAttribute
                                                              multiplier:1.0
                                                                constant:120.f]];

So everything is working with a plain view. Why doesn’t the features view work? The reason this view changed is because Instruments called it out as containing transparency. Image blending can really hurt scroll performance and you can find out which views have the feared transparency with Instruments – while running on a device with the Core Image instrument, check the “Color Blended Layers” box. You will find everything shaded green except for those views that use transparency, and they will be red. Red indicates SLOW. One way to get around this is to use opaque source images and arrange your layout so that no transparency is required. Another is to switch strategies, as in this case. A label using attributed strings created with a font designed with custom glyphs (replacing the original transparent UIImageViews) doesn’t trigger this warning. Examining the creation of the view reveals nothing to worry about. Strictly speaking this label could be added as a label rather than adding a label to a view and then using the view, but it remains the way it is to avoid touching too much other code that expects it to be a view. Experimenting with the internals of the view doesn’t change the way it left aligns on the centre either. Time to bring up the big gun, Reveal.

Looking at the UIView portion of Reveal’s inspector window for the feature view vs. the UILabel above it shows right away that there is no intrinsic content size set for the feature view, and it has a bounds of CGRectZero. This hasn’t mattered previously because the feature view has been used with conventional layout, explicit frame setting rather than constraints. What happens if an intrinsic content size is supplied for the custom class?

Adding intrinsic content size
1
2
3
4
5
6
7
8
  // after creation of the UILabel 
  [self invalidateIntrinsicContentSize];

// Later on in the class...
- (CGSize)intrinsicContentSize {

  return self.label.frame.size;
}

Running the app again shows that the view is just where it should be. A quick look in Reveal shows the intrinsic content size is as expected – clicking on the feature view shows its intrinsic content size is the same as for the label it contains, as we want. Adding a new view controller using constraint-based layout exposed an issue that didn’t cause problems in frame-based layout. Reveal uncovered the problem and verified the solution.


About Adam

Adam Eberbach is an iOS Developer at Itty Bitty Apps. He has worked on the iOS introspection tool Reveal and the Realestate.com.au iPhone/iPad App. You can follow him on twitter @aeberbach