Autolayout aspect ratio based on `intrinsicContentSize` (not constant) - ios

Is it possible to apply such auto layout constraints with aspect ratio calculated on the fly based on intrinsicContentSize?
In the documentation I've found only constrains with fixed ratio value.
Actual with and height from intrinsicContentSize is not important in my use case, I want preserve height and width ratio of the view which changes dynamically.
Should I provide my own implementation of the constraint? Or is there a better way?

The intrinsicContentSize is not available as input to constraints. (Logically, there are constraints that implement the intrinsicContentSize and related content-hugging and compression-resistance priorities, but that's different.)
If you want such an aspect ratio constraint, you'll have to add it yourself. It's easy enough to query the intrinsicContentSize at a given moment, verify that it provides real values (not NSViewNoInstrinsicMetric) for both dimensions, compute the aspect ratio, and then use that as the multiplier in a constraint that relates the item's width to its height.
The hard part is knowing when the intrinsicContentSize has been invalidated so you can remove the old aspect ratio constraint and add a new one. You can do that as a subclass of the view by overriding -invalidateIntrinsicContentSize. (Be sure to call through to super!) However, I don't know of a way to do that from a controller or superview.

You can find the answer on how to set up ratio-based constraints here. All you need is to constrain the width and the height together, maintaining a given aspect ratio (in that case 4/3).
We can debate whether it's a good thing views know this information or whether should their parents set this kind of constraints. I usually prefer parents to set constraints, but if your view doesn't make any sense without this constraint or all these views need this constraint, you can safely let these views manage their own width/height ratio.
Finally, intrinsicContentSize tells Auto Layout the size of the view when the view is alone:
Returns the natural size for the receiving view, considering only properties of the view itself.
I don't know what your view represents, but as the documentation says, you can return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric):
If a custom view has no intrinsic size for a given dimension, it can return UIViewNoIntrinsicMetric for that dimension.

If you have the situation where you want to calculate the value of a constraint during runtime, the best option is to CTRL drag the constraint into the .h file of your controller and create an IBOutlet for it. This allows you to change the value of a constraint in code. You can even animate the change to a constraint value.
Then in your code at setup time or when an action occurs which might change the value you want (like loading a new image for example) you calculate the value you want for the constraint and set its value in the code. Usually you do this using:
self.myConstraintIBOutlet.constant = <insert your new value>;
You may then need to mark the affected view as needing layout:
// Mark whole view as needing layout. You could do this in a subview if
// only a small area is affected
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
If you want a smooth transition, you can put layoutIfNeeded inside an animation block causing it to animate the change of constraint:
[self.view setNeedsLayout];
[UIView animateWithDuration:.25 animations:^{
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
// Completion code
}];

Related

Auto Layout with relative constraints not affecting systemLayoutSizeFittingSize: for UITableViewCell

I have a UITableView with dynamic height UITableViewCells and I'm using auto layout to calculate the heights as per this excellent answer on the topic. So far so good.
I'm adapting the app to work with the larger iPhone (6 and 6 Plus) screen sizes and it's mostly pretty straightforward.
However, in some of my cells I have an image that I want to span the whole width of the cell, and I want the image's height to be proportional to the width of the image (0.55 * the width, to be specific). Up to now I had the width and height of the image hard coded in the auto layout constraints, based on the standard 320px portrait table view width pre iPhone 6/6 Plus.
I thought it would be straightforward to add a relative height constraint like so (I'm using PureLayout):
[self.myImage autoMatchDimension:ALDimensionHeight
toDimension:ALDimensionWidth
ofView:self
withMultiplier:0.55f
relation:NSLayoutRelationGreaterThanOrEqual];
If you're not familiar with PureLayout, this translates to a call to
[NSLayoutConstraint constraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant:0.0f]
There are other constraints that pin it's edges to the superview, which is the UITableViewCell contentView.
However, when I call systemLayoutSizeFittingSize: on the cell's contentView it seems to totally ignore the relative height constraint, and the resulting cell height is far too small to fit the image.
If I set an explicit height constraint instead of a relative one, there's no problem. Similarly, if I subclass UIImageView and return an explicit size in intrinsicContentSize, there is no problem.
I even tried the following in my UIImageView subclass:
- (void) layoutSubviews {
[super layoutSubviews];
self.intrinsicSizeForAutolayout = self.frame.size;
[super layoutSubviews];
}
- (CGSize)intrinsicContentSize {
return self.intrinsicSizeForAutolayout;
}
where intrinsicSizeForAutolayout is a property I defined for the purpose. I thought this might work similarly to the way that setting preferredMaxLayoutWidth for UILabels solves a similar problem.
But no. It doesn't work.
It would seem I have little alternative to use ugly screen width checking code to conditionally set a fixed height constraint depending on the screen width, something I really wanted to avoid, as it kind of defeats the purpose of using auto layout in the first place.
The width of the cell's contentView doesn't any have constraints by default (its width is set by the SDK only when it is added to the table view), so when you call systemLayoutSizeFittingSize: on it, the constraint solver assumes it's valid to compress the width as much as needed when trying to find a valid solution, which of course results in the wrong height.
To fix this, you can add a single constraint to the contentView that fixes its width to the final cell/table view width. This works because that constraint will be factored into the cell sizing layout pass and result in systemLayoutSizeFittingSize: working as expected.
Using PureLayout, I recommend doing something like:
[UIView autoSetPriority:UILayoutPriorityRequired - 1 forConstraints:^{
[cell.contentView autoSetDimension:ALDimensionWidth toSize:CGRectGetWidth(tableView.bounds)];
}];
Note that it's not a bad idea to set a priority of less than Required for the constraint, just because this is simply to aid the sizing operation, and you don't want an exception if this gets broken (perhaps by a future SDK change in how table view cells work). But this probably doesn't matter.
Also, as usual you probably want to make sure this constraint is only added once (not every time tableView:heightForRowAtIndexPath: is called) -- this should be easy to do.
See here for a specific solution to your fork of the example project: https://gist.github.com/smileyborg/0a2082a4d26fcc7fde4d

Proper usage of intrinsicContentSize and sizeThatFits: on UIView Subclass with autolayout

I'm asking this (somehow) simple question just to be finicky, because sometimes I'm worried about a misuse I might be doing of many UIView's APIs, especially when it comes to autolayout.
To make it super simple I'll go with an example, let's assume I need an UIView subclass that has an image icon and a multiline label; the behaviour I want is that the height of my view changes with the height of the label (to fit the text inside), also, I'm laying it out with Interface builder, so I have something like this:
with some constraints that give fixed width and height to the image view, and fixed width and position (relative to the image view) to the label:
Now, if I set some text to the label, I want the view to be resized in height to fit it properly, or remain with the same height it has in the xib.
Before autolayout I would have done always something like this:
In the CustoView subclass file I would have overridden sizeThatFits: like so:
- (CGSize) sizeThatFits:(CGSize)size{
//this stands for whichever method I would have used
//to calculate the height needed to display the text based on the font
CGSize labelSize = [self.titleLabel intrinsicContentSize];
//check if we're bigger than what's in ib, otherwise resize
CGFloat newHeight = (labelSize.height <= 21) ? 51: labelSize.height+20;
size.height = newHeight;
return size;
}
And than I would have called something like:
myView.titleLabel.text = #"a big text to display that should be more than a line";
[myView sizeToFit];
Now, thinking in constraints, I know that autolayout systems calls intrinsicContentSize on the view tree elements to know what their size is and make its calculations, therefore I should override intrinsicContentSize in my subview to return the exact same things it returns in the sizeThatFits: method previously shown, except for the fact that, previously, when calling sizeToFit I had my view properly resized, but now with autolayout, in combination with a xib, this is not going to happen.
Of course I might be calling sizeToFit every time I edit text in my subclass, along with an overridden intrinsicContentSize that returns the exact same size of sizeThatFits:, but somehow I don't think this is the proper way of doing it.
I was thinking about overriding needsUpdateConstraints and updateConstraints, but still makes not much sense since my view's width and height are inferred and translated from autoresizing mask from the xib.
So long, what do you think is the cleanest and most correct way to make exactly what I show here and support fully autolayout?
I don't think you need to define an intrinsicContentSize.
Here's two reasons to think that:
When the Auto Layout documentation discusses intrinsicContentSize, it refers to it as relevant to "leaf-views" like buttons or labels where a size can be computed purely based on their content. The idea is that they are the leafs in the view hierarchy tree, not branches, because they are not composed of other views.
IntrinsicContentSize is not really a "fundamental" concept in Auto Layout. The fundamental concepts are just constraints and the attributes bound by constraints. The intrinsicContentSize, the content-hugging priorities, and the compression-resistance priorities are really just conveniences to be used to generate internal constraints concerning size. The final size is just the result of those constraints interacting with all other constraints in the usual way.
So what? So if your "custom view" is really just an assembly of a couple other views, then you don't need to define an intrinsicContentSize. You can just define the constraints that create the layout you want, and those constraints will also produce the size you want.
In the particular case that you describe, I'd set a >=0 bottom space constraint from the label to the superview, another one from the image to the superview, and then also a low priority constraint of height zero for the view as a whole. The low priority constraint will try to shrink the assembly, while the other constraints stop it from shrinking so far that it clips its subviews.
If you never define the intrinsicContentSize explicitly, how do you see the size resulting from these constraints? One way is to force layout and then observe the results.
Another way is to use systemLayoutSizeFittingSize: (and in iOS8, the little-heralded systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:). This is a closer cousin to sizeThatFits: than is intrinsicContentSize. It's what the system will use to calculate your view's appropriate size, taking into account all constraints it contains, including intrinsic content size constraints as well as all the others.
Unfortunately, if you have a multi-line label, you'll likely also need to configure preferredMaxLayoutWidth to get a good result, but that's another story...

resize superview after subviews change dynamically using autolayout

I cant for the love of god the the hang of this resizing superview.
I have a UIView *superview with 4 UILabels. 2 function as header for the 2 others.
The content in all 4 are dynamic coming from database.
SizeToFit vs SizeThatFits:(CGSize) vs UIView systemLayoutSizeFittingSize:, passing either UILayoutFittingCompressedSize or UILayoutFittingExpandedSize.
I use autolayout programatically and have set the superview height to be equal or greater to a dummy number.
where and how do I use these SizeToFit vs sizeThatFits:(CGSize) vs UIView systemLayoutSizeFittingSize:, passing either UILayoutFittingCompressedSize or UILayoutFittingExpandedSize. I have read a lot of tips here on stack but ended up with nothing.
DO I need to recalculate the constraints for the superview somewhere specific. Maby setting the height to be ´#property` in its controller class and remove and readd it?
Atm I have tried to put everything everywhere and then some. Still I get the same size end result with the dummy height and text floating outside. Even after setting clipsToBound on subview.
I am scratching my hair of.. help
If you're using Auto Layout, here's what you need to do:
Make sure you aren't adding fixed width and/or height constraints to any of your subviews (depending on which dimension(s) you want to dynamically size). The idea is to let the intrinsic content size of each subview determine the subview's height. UILabels come with 4 automatic implicit constraints which will (with less than Required priority) attempt to keep the label's frame at the exact size required to fit all the text inside.
Make sure that the edges of each label are connected rigidly (with Required priority constraints) to the edges of each other and their superview. You want to make sure that if you imagine one of the labels growing in size, this would force the other labels to make room for it and most importantly force the superview to expand as well.
Only add constraints to the superview to set its position, not size (at least, not for the dimension(s) you want it to size dynamically). Remember that if you set the internal constraints up correctly, its size will be determined by the sizes of all the subviews, since its edges are connected to theirs in some fashion.
That's it. You don't need to call sizeToFit or systemLayoutSizeFittingSize: to get this to work, just load your views and set the text and that should be it. The system layout engine will do the calculations for you to solve your constraints. (If anything, you might need to call setNeedsLayout on the superview...but this shouldn't be required.)
Use container views
In the following example I have a 30x30 image, and the UILabel is smaller than the containing view with the placeholder text. I needed the containing view to be at least as big as the image, but it needed to grow to contain multi-line text.
In visual format the inner container looks like this:
H:|-(15.0)-[image(30.0)]-(15.0)-[label]-(15.0)-|
V:|[image(30.0)]|
V:|[label(>=30.0)]|
Then, set the containing view to match the height of the label. Now the containing view will ride the size of the label.
As #smileyborg pointed out in his answer, connecting the content rigidly to the superview informs the layout engine that the simple container view should cause it to grow.
Yellow alignment rectangles
If you want the yellow alignment rectangles add -UIViewShowAlignmentRects YES in your scheme's list of run arguments.
This almost follows #smileyborg answer and comes with a concrete example.
Won't describe all constraints, but those related to the calculation of the height of UI objects.
[Label] Labels must not have a fixed height constraint, in this case, AutoLayout won't resize labels to fit the text, so setting edge constraints is the key. (green arrows)
[Subview] Steps 1 and 3 are very easy to follow, but this step can be misunderstood. As in the case with labels, subviews must not have height constraint set. All subviews must have top constraint set, ignoring bottom constraint, which can make you think will trigger unsatisfied constraint exception at runtime, but it won't if you set bottom constraint for the last subview. Missing to do so will blow the layout. (red arrows)
[Superview] Set all constraints the way you need, but pay big attention to the
height constraint. Assign it a random value, but make it optional, AutoLayout will set the height exactly to fit the subviews. (blue arrows)
This works perfectly, there is no need to call any additional system-layout update methods.
This was made dramatically easier with the introduction of Stack Views in iOS 9. Use a stack view inside your view to contain all your content that resizes, and then simply call
view.setNeedsUpdateConstraints()
view.updateConstraintsIfNeeded()
view.setNeedsLayout()
view.layoutIfNeeded()
after changing your content. Then you can get your new size by calling
view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
if you ever need to calculate the exact size required for a view.

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

iOS: Multi-line UILabel in Auto Layout

I'm having trouble trying to achieve some very basic layout behavior with Auto Layout. My view controller looks like this in IB:
The top label is the title label, I don't know how many lines it will be. I need the title label to display all lines of text. I also need the other two labels and the small image to be laid out right below the title, however tall it happens to be. I have set vertical spacing constraints between the labels and small image, as well as a top spacing constraint between the title label and its superview and a bottom spacing constraint between the small image and its superview. The white UIView has no height constraint, so it should stretch vertically to contain its subviews. I have set the number of lines for the title label to 0.
How can I get the title label to resize to fit the number of lines required by the string? My understanding is that I can't use setFrame methods because I'm using Auto Layout. And I have to use Auto Layout because I need those other views to stay below the title label (hence the constraints).
How can I make this happen?
Use -setPreferredMaxLayoutWidth on the UILabel and autolayout should handle the rest.
[label setPreferredMaxLayoutWidth:200.0];
See the UILabel documentation on preferredMaxLayoutWidth.
Update:
Only need to set the height constraint in storyboard to Greater than or equal to, no need to setPreferredMaxLayoutWidth.
Expand your label set number of lines to 0 and also more importantly for auto layout set height to >= x. Auto layout will do the rest. You may also contain your other elements based on previous element to correctly position then.
Source: http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html
Intrinsic Content Size of Multi-Line Text
The intrinsic content size of UILabel and NSTextField is ambiguous for multi-line text. The height of the text depends on the width of the lines, which is yet to be determined when solving the constraints. In order to solve this problem, both classes have a new property called preferredMaxLayoutWidth, which specifies the maximum line width for calculating the intrinsic content size.
Since we usually don’t know this value in advance, we need to take a two-step approach to get this right. First we let Auto Layout do its work, and then we use the resulting frame in the layout pass to update the preferred maximum width and trigger layout again.
- (void)layoutSubviews
{
[super layoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[super layoutSubviews];
}
The first call to [super layoutSubviews] is necessary for the label to get its frame set, while the second call is necessary to update the layout after the change. If we omit the second call we get a NSInternalInconsistencyException error, because we’ve made changes in the layout pass which require updating the constraints, but we didn’t trigger layout again.
We can also do this in a label subclass itself:
#implementation MyLabel
- (void)layoutSubviews
{
self.preferredMaxLayoutWidth = self.frame.size.width;
[super layoutSubviews];
}
#end
In this case, we don’t need to call [super layoutSubviews] first, because when layoutSubviews gets called, we already have a frame on the label itself.
To make this adjustment from the view controller level, we hook into viewDidLayoutSubviews. At this point the frames of the first Auto Layout pass are already set and we can use them to set the preferred maximum width.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[self.view layoutIfNeeded];
}
Lastly, make sure that you don’t have an explicit height constraint on the label that has a higher priority than the label’s content compression resistance priority. Otherwise it will trump the calculated height of the content. Make sure to check all the constraints that can affect label's height.
I was just fighting with this exact scenario, but with quite a few more views that needed to resize and move down as necessary. It was driving me nuts, but I finally figured it out.
Here's the key: Interface Builder likes to throw in extra constraints as you add and move views and you may not notice. In my case, I had a view half way down that had an extra constraint that specified the size between it and its superview, basically pinning it to that point. That meant that nothing above it could resize larger because it would go against that constraint.
An easy way to tell if this is the case is by trying to resize the label manually. Does IB let you grow it? If it does, do the labels below move as you expect? Make sure you have both of these checked before you resize to see how your constraints will move your views:
If the view is stuck, follow the views that are below it and make sure one of them doesn't have a top space to superview constraint. Then just make sure your number of lines option for the label is set to 0 and it should take care of the rest.
I find you need the following:
A top constraint
A leading constraint (eg left side)
A trailing constraint (eg right side)
Set content hugging priority, horizontal to low, so it'll fill the given space if the text is short.
Set content compression resistance, horizontal to low, so it'll wrap instead of try to become wider.
Set the number of lines to 0.
Set the line break mode to word wrap.
None of the different solutions found in the many topics on the subject worked perfectly for my case (x dynamic multiline labels in dynamic table view cells) .
I found a way to do it :
After having set the constraints on your label and set its multiline property to 0, make a subclass of UILabel ; I called mine AutoLayoutLabel :
#implementation AutoLayoutLabel
- (void)layoutSubviews{
[self setNeedsUpdateConstraints];
[super layoutSubviews];
self.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds);
}
#end
I have a UITableViewCell which has a text wrap label. I worked text wrapping as follows.
1) Set UILabel constraints as follows.
2) Set no. of lines to 0.
3) Added UILabel height constraint to UITableViewCell.
#IBOutlet weak var priorityLabelWidth: NSLayoutConstraint!
4) On UITableViewCell:
priorityLabel.sizeToFit()
priorityLabelWidth.constant = priorityLabel.intrinsicContentSize().width+5
One way to do this...
As text length increases try to change (decrease) the fontsize of the label text using
Label.adjustsFontSizeToFitWidth = YES;

Resources