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

1

68

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:

simple view image

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:

simple view image, constraints shown

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?

Lester answered 9/6, 2014 at 18:57 Comment(0)
T
85

I don't think you need to define an intrinsicContentSize.

Here's two reasons to think that:

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

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

Tait answered 14/10, 2014 at 5:6 Comment(6)
Good tip with the systemLayoutSizeFittingSize:… methods, thanks. Are there particular issues with UILabel's preferredMaxLayoutWidth? It can just be made automatic and it'll expand to fit (other constraints), no?Cowberry
You need to set preferredMaxLayoutWidth in order for intrinsicContentSize to return a size that uses line-wrapping (the text and the preferredMaxLayoutWidth are the only intrinsic content, I believe). The only way to set it automatically, I think, is to override layoutSubviews somewhere and set it based on completed layout values. In contrast, systemLayoutSizeFittingSize: looks at all constraints, not just those from intrinsicContentSize. So it will be affected by (1) other constraints affecting width, and (2) the combo of numberOfLines=0 and its argument targetSizeTait
I'm struggling with this as well and I can't seem to be able to set the low priority constraint of height zero for the view as a whole. Interface Builder doesn't allow me to set any constraints on the main view of the .xib file. Any idea why?Victimize
Interestingly, iOS 9's new UIStackView uses intrinsicContentSize to calculate the size of its subviews rather than using sizeThatFits etc.. This more or less forces the developer to override the method and call sizeThatFits directly :/Jerlenejermain
Hmm, really? I'd be quite surprised if UIStackView uses intrinsicContentSize directly. I'd expect it uses either the short or long version of systemLayoutSizeFittingSize:. And if that's the case, then it should suffice just to establish appropriate internal constraints, rather than override intrinsicContentSize. My understanding is that the only effect of having an intrinsicContentSize is that it causes the system to automatically generate certain non-required constraints that express that preferred size.Tait
I can't agree with you. If you want a frame-based layout compound view to work like a constraint-base view, you need to implement intrinsicContentSize just like UILabel. When mixing auto-layout and frame-based manual layout, you need to do that.Soliloquize

© 2022 - 2024 — McMap. All rights reserved.