Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autolayout - stretching a view to fill its parent view

I noticed some very strange behavior when trying to fill a view with a child view using autolayout. The idea is very simple: add a subview to a view and make it use all of the width of the parent view.

NSDictionary *views = @{
                        @"subview":subView,
                        @"parent":self
                       };

This does not work:

[self addConstraints:
      [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[subview]|" 
                                              options:0 
                                              metrics:nil 
                                                views:views]];

The subview doesn't use the full width of the parent view.

But this works:

[self addConstraints:
      [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[subview(==parent)]|" 
                                              options:0 
                                              metrics:nil 
                                                views:views]];

I would expect that both would work as intended. So why is the first example not working? It is what Apple recommends in the following technical note:

https://developer.apple.com/library/ios/technotes/tn2154/_index.html

EDIT: (removed irrelevant information)

like image 335
Philippe Leybaert Avatar asked Oct 04 '14 22:10

Philippe Leybaert


3 Answers

Here are the constraints you posted for your “first example”:

first case

Here are the constraints you posted for your “2nd example”:

second case

I see two differences in these structures, which I have highlighted in red in the diagrams:

  1. The first (broken) case has a constraint (0x8433f8f0) on the near-root UIView 0x8433f8f0, pinning its width to 320 points. This is redundant, because the bottom-level views are constrained to 160 points each, and there are sufficient constraints to make all the ancestors of those narrower views be 320 points wide.

  2. The second (working) case has a constraint (0x7eb4a670) pinning the width of the near-bottom UIView 0x7d5f3fa0 to the width of the DetailWeatherView 0x7d5f3270. This constraint is redundant because 3fa0's left edge is pinned to 3270's left edge, and 3fa0's right edge is constrained to 3fa0's right edge. I assume the (==parent) predicate adds this constraint.

So you might think, each case has one redundant constraint, so what? No big deal, right?

Not quite. In the second case, the redundant constraint is truly harmless. You could change the width of either (or both) of WeatherTypeBoxView and AdditionalWeatherInfoBox, and you'd still get a unique solution.

In the first case, the redundant constraint is only harmless if the width of the views doesn't change. If you change the 320-width constraint on f8f0 without changing the 160-width constraints on the leaf views, the constraint system has no solution. Autolayout will break one of the constraints to solve the system, and it might well break that 320-width constraint. And we can see that the 320-width constraint, with its UIView-Encapsulated-Layout-Width annotation, is imposed by the system to force this view hierarchy to conform to some container's size (maybe the screen size; maybe a container view controller's imposed size).

I don't know why your first case has this extra top-level constraint and your second doesn't. I'm inclined to believe you changed something else that caused this difference, but autolayout is mysterious enough that I'm not 100% convinced of that either.

If your goal is to make this view hierarchy fill its container, and keep the leaf views equal width, then get rid of the 160-width constraints on the leaf views and create a single equal-width constraint between them.

like image 85
rob mayoff Avatar answered Nov 14 '22 20:11

rob mayoff


EDIT: as Ken Thomases has pointed out, I'm completely wrong here!

Yes, both of the constraint systems you specify should cause the parent and subview (or parent and contentView) to have the same width.

So I would ask, are you absolutely sure that what you're seeing is the contentView fail to fill the parent? Is it possible what you're seeing in the broken case is that the parent is actually being shrunk to fit the contentView, and you're not noticing it because the parent has a transparent or otherwise invisible background?

To check this, set the contentView and the parent to have different opaque background colors. If I am wrong, you will clearly see a region of background where the parent extends out with a width greater than the contentView. If I am right, the contentView will completely cover the parent's width.

Here is why I am (maybe presumptuously) questioning your stated fact about what you are seeing:

The key difference in your log outputs are that, for the broken case, we see an unexpected constraint that holds a UIView to a fixed width of 320:

<NSLayoutConstraint:0x8433f8f0 'UIView-Encapsulated-Layout-Width' H:[UIView:0x841b9f40(320)]>

What view is that? Based on the constraint name "UIView-Encapsulated-Layout-Width", that it is associated with a UIViewControllerWrapperView, and that the width is being held to 320 (width of an iPhone), we can deduce this constraint is automatically created by UIKit to constrain the size of a UICollectionView.view to the size of the screen. Looking at all the constraints, you can further see that this width=320 constraint is passed down via a chain of constraints (encapsulated-view -> DetailView -> DetailWeatherView) until it ultimately determines the width of DetalWeatherView, which is your parent, and which uses superview-edge-offset constraints to hug the contentView, which should therefore also get that same width of 320.

In contrast, for the case that works, you can also see the same chain of constraints that should constraint the contentView.width to equal the width of the top encapsulated layout view. But the difference is there is no constraint anywhere that holds anything to a fixed value of 320. Instead, there is only a left-alignment constraint.

So I would expect to see that in both cases contentView.width == parent.width, but that in the broken case width=320 whereas in the working case the width is being determined by lower-priority internal constraints within contentView that allow it to expand to a value greater than 320, perhaps to its intrinsicContentSize.

like image 41
algal Avatar answered Nov 14 '22 20:11

algal


The reason this was not working as expected was this:

The parent view is a UISCrollView, and the horizontal constraints are set to "H:|[contentView]|", the contentSize of the scrollview will adjust itself to the width requested by contentView, not the other way around. So the autolayout engine will first determine the dimensions of contentView and then adjust the contentSize of the parent (scrollview) to the same width. If contentWidth is narrower than the parent, it won't stretch because the contentSize of the parent (scrollview) will shrink to the size of contentView.

For regular views (not scrollviews), the width of the parent view is fixed so it will first layout the parent view and then the child view(s).

By forcing the width of contentView to the same width as the parent scrollView, contentView will always be the same width as the parent scrollview, which is what I wanted (and expected).

like image 43
Philippe Leybaert Avatar answered Nov 14 '22 21:11

Philippe Leybaert