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

47,007

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

Share:
47,007
dev_mush
Author by

dev_mush

I work as a full stack software developer (mainly focused on development and deployment of mobile products) at ufirst, an Italian startup which aim is to solve one of the most tedious problems of the world: Queues! Now my focus is oriented in building great apps with Flutter. In my free time I try to be in the present. Constantly seeking for new stuff to discover, mildly interested in generative arts obsessed by free climbing, piano playing and pizza🍕.

Updated on July 21, 2020

Comments

  • dev_mush
    dev_mush almost 4 years

    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?

  • Benjohn
    Benjohn over 9 years
    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?
  • algal
    algal over 9 years
    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 targetSize
  • Bruno Morgado
    Bruno Morgado about 9 years
    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?
  • fatuhoku
    fatuhoku over 8 years
    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 :/
  • algal
    algal over 8 years
    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.
  • DawnSong
    DawnSong over 3 years
    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.