Proper usage of intrinsicContentSize and sizeThatFits: on UIView Subclass with 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...
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, 2020Comments
-
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:
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 overrideintrinsicContentSize
in my subview to return the exact same things it returns in thesizeThatFits:
method previously shown, except for the fact that, previously, when callingsizeToFit
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 overriddenintrinsicContentSize
that returns the exact same size ofsizeThatFits:
, but somehow I don't think this is the proper way of doing it.I was thinking about overriding
needsUpdateConstraints
andupdateConstraints
, 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 over 9 yearsGood tip with the
systemLayoutSizeFittingSize:…
methods, thanks. Are there particular issues withUILabel
's preferredMaxLayoutWidth? It can just be made automatic and it'll expand to fit (other constraints), no? -
algal over 9 yearsYou need to set
preferredMaxLayoutWidth
in order forintrinsicContentSize
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 overridelayoutSubviews
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 ofnumberOfLines=0
and its argumenttargetSize
-
Bruno Morgado about 9 yearsI'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 over 8 yearsInterestingly, iOS 9's new
UIStackView
usesintrinsicContentSize
to calculate the size of its subviews rather than usingsizeThatFits
etc.. This more or less forces the developer to override the method and callsizeThatFits
directly :/ -
algal over 8 yearsHmm, really? I'd be quite surprised if
UIStackView
usesintrinsicContentSize
directly. I'd expect it uses either the short or long version ofsystemLayoutSizeFittingSize:
. And if that's the case, then it should suffice just to establish appropriate internal constraints, rather than overrideintrinsicContentSize
. My understanding is that the only effect of having anintrinsicContentSize
is that it causes the system to automatically generate certain non-required constraints that express that preferred size. -
DawnSong over 3 yearsI 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 likeUILabel
. When mixing auto-layout and frame-based manual layout, you need to do that.