BuySellAds.com

Our DNA is written in Objective-C
Jump

Don’t Cross The Streams

Like most people I could get buy blissfully without dealing too much with Auto Layout. But there are layout-heavy scenarios where you start having the feeling that they might be much easier to solve using auto layout than by setting autoresizing masks (aka “struts and springs”).

One such example is the layout of controls in an inspector panel where I laid out the controls distanced from each other with layout constraints in Interface Builder. Architecture-wise I am using such an “inspector palette” in a DMInspectorPalette which gives me several collapsible sections. These are contained in a NSTabView (using DMTabBar as header) to get multiple inspectors laid out like in Xcode. And these are the right-most view in an NSSplitView to allow resizing of the inspector panel.

I was having some issues with that beginning with 10.9 which I’d like to document here.

Beginning with OS X 10.9 I started to see enormous tearing on something that was working without problems on earlier Mac OS versions. It’s as if the subviews of the tab view would get their frames updated much later than the split view divider was being moved. Funny thing: this only occurred on my 2 year old MacBook Air, not only my 27″ iMac, both are running the GM of 10.9.

Also I was not able to record the issue occurring with Quicktime, while I was recording the panel was always behaving perfectly.  So I recorded it with my iPhone. Look at “Info” how that lags behind the gray bar above it.

Tearing

On OS X and iOS alike Auto Layout is off by default and gets enabled as soon as any subview in the window’s hierarchy has a layout constraint added. That could either be by adding a subview loaded from Interface Builder that has the Auto Layout checkbox checked, or by adding a constraint to a view in code. It says so in the docs.

Let me repeat that: add a single constraint and the entire window will be using Auto Layout. This is true for both iOS and OSX.

Auto Layout Emulates Struts & Springs

Normally you should not notice a difference because the OS automatically translates all autoresizing masks into equivalent layout constraints that behave exactly the same. Though as my example has shown the timing between both systems might be slightly different.

In my code the frame of the Info panel was set via setFrame: whereas the gray tab bar with icon was set via autoresizing mask. Adding the panel contents – which is using Auto Layout in IB – moved the tab bar to be using equivalent layout constraints. It seems to me that one a sufficiently fast Mac you should not see a difference, but in my case I’m redrawing a large page image on every pixel the divider is moved and that stressed my Intel GPU sufficiently that it didn’t get around to calling setFrame: often enough.

Interestingly I found that removing the setFrame and setting autoresizing masks on both the bar and the content view did nothing to improve the tearing. Only converting it to be using Auto Layout fixed the problem.

To make the adding of a total of 8 constraints a little easier I created a helper method.

- (void)addLayoutConstraintsForSubview:(NSView *)subview  edgeInsets:(NSEdgeInsets)edgeInsets
{
   NSParameterAssert(subview);
   //NSAssert(subview.superview == self, @"Can only pin a direct subview of the receiver");
 
   // subview cannot have autoresizing mask, that would interfere with these
   subview.translatesAutoresizingMaskIntoConstraints = NO;
 
   NSMutableArray *tmpArray = [NSMutableArray array];
 
   if (edgeInsets.left >= 0)
   {
      // subview's left is x points from superview's left
      NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:edgeInsets.left];
      [tmpArray addObject:constraint];
   }
   else
   {
      // subview's left is x points from superview's right
      NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:edgeInsets.left];
      [tmpArray addObject:constraint];
   }
 
   if (edgeInsets.right >= 0)
   {
      // subview's right is x points from superview's right
      NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:-edgeInsets.right];
      [tmpArray addObject:constraint];
   }
   else
   {
      // subview's right is x points from superview's left
      NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:-edgeInsets.right];
      [tmpArray addObject:constraint];
   }
 
   if (edgeInsets.top >= 0)
   {
      // subview's top is x points from superview's top
      NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:edgeInsets.top];
      [tmpArray addObject:constraint];
   }
   else
   {
      // subview's top is x points from superview's bottom
      NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:edgeInsets.top];
      [tmpArray addObject:constraint];
   }
 
   if (edgeInsets.bottom >= 0)
   {
      // subview's bottom is x points from superview's bottom
      NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-edgeInsets.bottom];
      [tmpArray addObject:constraint];
   }
   else
   {
      // subview's bottom is x points from superview's top
      NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:-edgeInsets.bottom];
      [tmpArray addObject:constraint];
   }
 
   [self addConstraints:tmpArray];
}

This method allows me to express a subview’s constraints relative to its superview in terms of edge insets. Positive values means a distance from the corresponding edge. Negative values mean the opposite edge.

So for the top bar which is 22 points high but otherwise follows its superview we can specify:

[self addLayoutConstraintsForSubview:_tabBar edgeInsets:NSEdgeInsetsMake(0, 0, -22, 0)];

The third value is negative in the bottom slot, which means that the bottom is 22 points from the top of the superview.

For the content view we specify:

[self addLayoutConstraintsForSubview:_tabView edgeInsets:NSEdgeInsetsMake(22, 0, 0, 0)];

We established before that having layout constraints on any view causes all autoresizing masks to be converted to layout constraints as well. This would mean in this case that we could end up with conflicting constraints and so we need to disable this automatic translation.

// subview cannot have autoresizing mask, that would interfere with these
subview.translatesAutoresizingMaskIntoConstraints = NO;

If you begin to adopt Auto Layout in code you probably have seen the kind of exception this produces if you have your own constraints conflict with autoresizing-constraints:

Auto Layout Exception

In this example the NSLayoutConstraint line is our own, the two NSAutoresizingMaskLayoutContraint are such automatically translated constraints.

I produced the above exception by setting a subview’s autoresizingMask to flexible height and width and then adding a constraint limiting the width to 30. Autoresizing Mask Layout Constraints are installed with a priority level of 1000 (required), the same as when you don’t specify a priority for your own level.

Thus if we modify our own constraint’s priority to 999 this exception is no longer occurring, since 999 is no longer required, but a very strong suggestion.

Interpreting NSAutoresizingMaskLayoutContraint

Those system constraints participate in the Auto Layout system like other constraints, but they are specialized to be able to constrain multiple layout attributes using only a single layout constraint object.

I’ve often wondered as to how to read the log output, Stack Overflow user jrturton figured it out.

“…the logging format is pretty straightforward.

  • h= or v= indicates that we are talking about contraints in the horizontal or vertical direction.
  • - indicates a fixed size
  • & indicates a flexible size
  • The order of symbols represents margin, dimension, margin

Therefore, h=&-& means you have flexible left and right margins and a fixed width, v=-&- means fixed top and bottom margins and flexible height, and so forth.”

Conclusion

The obvious conclusion if what this article derived its title from: Don’t cross the streams.

Apple does make it very simple for us to migrate to Auto Layout by automatically translating autoresizing masks to internal constraints. And for the most part this works without you even noticing that something differently is happening.

But there are cases – like I have demonstrated here – where this switching to Auto Layout has problems with some custom views which still have an overwritten setFrame: method. If you start seeing such weird issues you know that it is time to completely move to using layout constraints.

For the longest time I wondered how I could get the same behavior in code as you get when checking the Enable Auto Layout checkbox for a view in Interface Builder. My eyes also got opened by the information that Auto Layout is off by default and will get enabled for the entire window as soon as there is a single layout constraint anywhere.

Put simpler: there is is no such equivalent in code. This switch only tells IB to create layout constraints at design time. And once this view is added to a window, it will enable Auto Layout for everything.


Categories: Q&A

%d bloggers like this: