Autolayout - stretching a view to fill its parent view - ios

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)

Here are the constraints you posted for your “first example”:
Here are the constraints you posted for your “2nd example”:
I see two differences in these structures, which I have highlighted in red in the diagrams:
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.
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.

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.

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).

Related

Basic Auto Layout - nested width and dynamic height

Simple goal, but it demonstrates situations where Auto Layout gives me a headache. I want to have a stack view with a width of 256pt and a dynamic height based on layout (I shouldn't have to manually specify the height).
Inside it should be an image view sized 64pt x 64pt, which should also be centered horizontally as well as constrained 8pt from the superview's top. Note that the image view isn't the only child, hence why the stack view's height must be sized dynamically.
Auto Layout now tells me there's a conflict between the 256pt width constraint of the stack view and the 64pt width constraint of the image, as well as some mysterious "leading = Image.leading" and "trailing = Image.trailing" conflict which I can't even delete nor find.
Am I missing out something here regarding Auto Layout? I expect all logic to be contained in the interface builder, so no code should be required.
Running Xcode 9.1
Layout image
There is nothing to confuse. iOS clearly telling you the issue.
StackViews take size based on the size of child components (this is called implicit size) unless its been overriden manually as in your case which is 256pt.
Because stackView is just a container for multiple childViews stacked either horizontally or vertically, now because you have added only one imageView to it, it adds the leading and trailing constraint to it which makes absolute sense because you added a single view to the stack of view's , now what should stackView do? stretch childView (in your case imageView) to its own size.
But then you did not allow it because you added width constraint to imageView now when it tries to increase the imageView's width imageView's constraint wont allow it.
Hence it is complaining that there are too many conflicting constraints. Thats all :)
some mysterious "leading = Image.leading" and "trailing =
Image.trailing" conflict which I can't even delete nor find.
You cant delete them because, imageView is the only view inside stackView. Because there is only one child view to stack, stackView will start from left side (leading) to right side (trailing). Because now stackView has its own width it tries to change the width of imageView to reflect the same! But images width constraint prevents it from happening.
What are you trying to achieve with imageView added to stackView. If there is only one view in stackView, adding stackView does not make any sense. Reconsider what you are doing.
Finally, when you have only one childView in stack view, adding horizontal center does not make any sense (no matter vertical/horizontal stackView).

UIScrollView's Content View is ignoring equal width constraint to main view on Xcode 6

I've added a Scroll View that contains a content view. The scrollview's constraints keep it just below my progress bar and attached to the leading, trailing, and bottom of the superview. The content view's constraints hold it to the sides of the scroll view, with one additional constraint: equal the width of the superview.
Everything looks great on the storyboard preview, but at runtime the scrollview's calculated width increases.
Basically, it seems that AutoLayout is ignoring my constraint to constrain the width of the content view, and instead just allows the content view to get as large as it wants to fit the content in.
Here are my constraints:
Thank you so much for helping me get over this roadblock! I've been banging my head against the wall for days.
Here are some things I would try. I don't have comment privileges yet, otherwise I would ask for elaborations:
Are there any outputs in the console for Xcode? Generally when Autolayout is forced to break constraints, it tells you about it in the logs.
So to be clear, for the Content View, you have a constraint that sets the width equal to the ScrollView, as well as constraints to match the Leading and Trailing edges to the ScrollView?
If the constraint is not being followed then either the ScrollView is also expanding somehow, or the constraint conflicts with another constraint, and the width-matching constraint got broken somewhere along the way.

Why is my UIButton ignoring its autolayout constraint to stretch its height?

I have a button whose image is set dynamically at run time. As a result, to maintain the appropriate ratio, I have the following code in where the image is set:
[super removeConstraint:self.ratioConstraint];
float ratio=photo.size.height/photo.size.width;
self.ratioConstraint=[NSLayoutConstraint constraintWithItem:self.button
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.button
attribute:NSLayoutAttributeWidth
multiplier:ratio
constant:0];
[self.ratioConstraint setPriority:UILayoutPriorityDefaultHigh];
[super addConstraint:self.ratioConstraint];
I even applied a [self layoutSubviews] to let me check the frame. When I queried the frame, it was right. The ratio of 1 was honored and the height was set equal to the width.
But in ViewWillAppear, the same query produces the same impossible results: the height of the frame is stretching to let the image grow to it's full height. Despite having the above constraint, which should limit the height based on the existing (and honored) width limits.
Even more infuriating, until I set the image to something new at runtime (i. e. by taking the camera), the restraint appears to be functional -- the placeholder image is squashed down horizontally, and compacts itself vertically to fit. It's only once I take a photo that that height explodes, ignoring it's constraint for no apparent reason.
In case it matters, the above code is executed in a custom subclass of UIControl, which is then embedded in a UIView ('ContentView') which is itself embedded in a scroll view. The width restrictions are, more or less:
"ContentView" has a width equal to the ViewController's view.
The UIControl view then has it's width set to a value that either gives it about half or about one quarter of the screen's width, depending on the exact control. (I have five of them; one gets half the screen width, the others all get one quarter and are arranged in a row).
The UIControl then sets the UIButton's width equal to it's own via the constraint: H:|-(0)-[Button]-(0)-| .
Even more annoyingly, I ran through and inspected every constraint from the UIViewController's view down -- a bit of recursive logic that pulled up every constraint in a given view and it's subviews that applied to a given subject. I then ran that against each object in the view hierarchy, and got... zip. No constraints that were interfering. Nothing that should have effected the height of hte buttons in question except the code above, which apply the ratio constraint.
The solution actually proved to be rather embarrassingly obvious, though I'm not sure I understand why the solution worked (or the code above didn't).
In the code above, I used the constant 'UILayoutPriorityDefaultHigh'; in my init code, I used the constant 'UILayoutPriorityRequired'.
Apparently, the photo's desire to stretch out to it's full size is a high priority, so I could only override that 'silent' constraint by upping my own to required. (Interestingly, the bug actually existed before I added the priority calls at all, suggesting that a constraint doesn't start out with that priority -- something I suspect may confuse more than one programmer out there when they trip over it).

Autolayout height equal to MAX(multiple view heights)

Say I have a view called container. container contains 5 UIButtons. I want to add a height NSLayoutConstraint on container, and this height should be equal to the NSLayoutHeightAttribute of the tallest button in its subviews.
I don't see a straightforward way to do this. Anyone have any ideas?
You need one constraint for each subview (button), specifying that the container's height should be greater than or equal to the subview's height. Give that constraint a high priority, like UILayoutPriorityRequired (which is the default anyway).
Then add one more constraint on the container's height, specifying that it should have a height equal to zero. Give that constraint a low priority, like UILayoutPriorityLow. Since auto layout tries to minimize the error of unsatisfied constraints, it will make the container as short as possible while still satisfying all higher-priority constraints.
I have put an example in this gist. It produces this result:
The blue views have fixed heights. The tan view is the superview of the blue views and its height is constrained as I described above. I pinned each subview's bottom to the container's bottom, but you could pin the tops or the Y centers instead.

Using autolayout on iOS how can I specify that a view should take up as much space as possible?

I am using autolayout in iOS to try and build a layout with fluid widths. The visual format for the constraint I am currently using is:
[self.scrollViewContainer addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"H:|-(>=32)-[viewToAdd(<=576)]-(>=32)-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(viewToAdd)
]];
That is to say: I want a minimum of 32px spacing on either side, and I want the viewToAdd to have a maximum width of 576px. This works well except that I want the viewToAdd to use up any available space while still meeting all the constraints. Currently I get the viewToAdd only ever being as wide as its intrinsic content size, and the spacing growing as needed.
Is there a way to specify that the viewToAdd should be as large as possible?
You're going to have to specify additional constraints on the view in order to get it to size the view to fill the remaining available space. Currently, you are setting the view minimums and maximums, but not setting any concrete constraints to give autolayout a more complete solution. In addition to the upper bound for the width, you need to give it 1. a starting point to solve the width for the view by either explicitly giving it a width at a lower priority, or 2. give the spacing on either side some more rigid constraints.
Based on what you've described, you need to constrain your view to give it some more substantial bindings to the superview on either side in order to let the system solve for the width. Since you would like to have the view size itself based on it's container's size, you will need to modify the spacing constraints (option 2 from above). In the above instance, you have specified only a minimum spacing, which would result in any of the following constraint solutions being found as valid by the autolayout engine for a 400pt superview:
|-32pt-[20pt]-------348pt-| <-- autolayout will probably choose this one
|-100pt----[20pt]---280pt-|
|-50pt--[20pt-]-----330pt-|
Which is probably not what you are wanting. Even more still, the width of the view can be anything between 0-576pt, which is also probably not what you're wanting. Since autolayout doesn't know what you want, it's simply using the intrinsicContentSize of the view for concrete sizing constraints. Since you chose 32pt as the spacing, a first step would be to give the spacing constraints some more substantial instructions, namely, telling the system that the spacing should be 32pts between the edges of the view and the superview unless the width of the view is >576pts. You would do this like so in your VFL string:
"H:|-(>=32,==32#900)-[viewToAdd(<=576)]-(>=32,==32#900)-|"
This says: "viewToAdd should have a maximum width of 576pts and have padding between itself and it's superview of 32pts. If the size of the superview grows beyond the maximum width of viewToAdd plus the initial padding of 64pts, the padding on either side should grow in order to continue to solve the constraint set."
This results in the following constraints being correct for a 400pt superview:
|-32pt--[336pt]--32pt-|
If you would like for viewToAdd to remain centered in it's superview when the view grows beyond the maximum, you will have to pass in the option NSLayoutFormatAlignAllCenterX to the options parameter in [NSLayoutConstraint -constraintsWithVisualFormat:]. If you did not have the >=32 constraint set on the padding, or did not set a priority lower than 1000 ("required" constraint priority level) on your padding's ==32 constraint, then your superview would be unable to grow beyond 640pts.
Not sure if this exactly answers the question, but for the ones using Storyboard: I found that you can add a width-constraint and constraints for both the distance to the left and right side of the parent view for example. If you then change the width-constraint to '<=' instead of '=' and set the priority of the distance constraints (both leading and trailing) to 750, the view will max out to the max width, respecting the distance-constraints if the view is too small.

Resources