It's very common to want to adjust the bounds of
UILabel to fit its contents. The
most common technique I've seen from developers looks something like
UILabel *label = [[UILabel alloc] initWithFrame:...]; label.text = NSLocalizedString(@"Some long text here..."); label.numberOfLines = 0; CGSize maxSize = CGSizeMake(label.bounds.size.width, CGFLOAT_MAX); CGSize textSize = [label.text sizeWithFont:label.font constrainedToSize:maxSize]; label.frame = CGRectMake(10, 10, textSize.width, textSize.height);
This code is using one
sizeWithFont:... methods to
calculate the bounds of the label's text, taking its font and width
If you are sizing your labels like this, you need to read on because you are doing it wrong.
Let's take a step back and think about this for a moment.
We want to know how much space the label needs to display its
text. Surely that logic belongs in
UILabel? Only the
label itself really knows how it renders its content and what
offsets or margins it may be applying.
It seems like a bit of a smell to me that we have to calculate this stuff ourselves using methods on another class.
Now in the case of
turns out that
returns exactly the size we need. The label is probably doing a
which matches up perfectly with the results we get back from
sizeWithFont:... methods. But
is this always going to be true?
UILabel decides to add support for special
borders, or configurable line heights, or some other visual effects
that will change the required bounds? The above code is going to
break. It isn't future proof, and we are essentially duplicating or
guessing the behaviour of
UILabel really going to change in such a manner?
Well, it just did. As of iOS 6,
UILabel supports the
rendering of attributed strings. This means arbitrary ranges in the
label's text can have different fonts and styles applied to them. If
there is word in the middle of the label with a very large font,
bounds required to fit that string in the label are going to
If you use the previous code unmodified on a label containing an attributed string, you are going to get incorrect results.
As it turns out, that there are UIKit additions
provide similar functionality
-sizeWithFont: methods. But
our code still should not be duplicating this kind of logic,
especially when there is a far better and simpler solution...
If we want to resize a label to fit its contents, we can just tell it to do so:
Bam. Done. The label will take its current width, and adjust its height to fit its contents (assuming it is a multi-line label). If the label has a width of 0, its size is adjusted to fit everything on a single line.
If you wish to size the label without setting a frame beforehand, you can ask the label for the size it needs like this:
CGSize maxSize = CGSizeMake(200.0f, CGFLOAT_MAX); CGSize requiredSize = [label sizeThatFits:maxSize]; label.frame = CGRectMake(10, 10, size.width, size.height);
Note the use of
CGFLOAT_MAX here to
standard in UIKit and have existed for a very long time. They work
with the new attributed string support in iOS 6, and will continue
to work no matter what changes are made to
It pains me to see people writing useless (and often times
UILabel for something as standard as this. I
guess the lesson to take away is to explore as much of the
documentation as you can. I'm sure there are many useful methods out
there that I've overlooked.
Going a little deeper
What you need to know is that
overridable and returns the "most appropriate" size for the control
that fits the constraints passed to it. The method can
decide to ignore the constraints if they cannot be met.
sizeToFit will simply call through
sizeThatFits: passing the view's current size as the
argument. It will then update the view's frame based on the value it
gets back. So all the important logic goes
sizeThatFits:, and this is the method you should
override for your own custom controls.
A major detour:
UIButton insets demystified
Many of the standard UIKit controls
sizeThatFits:, one of which
UIButton. However, things can get a little tricky
UIButton, especially when when you throw insets
into the mix.
We'll start by creating a button with an image, a stretchable background image, and some text:
UIButton *b = [UIButton buttonWithType:UIButtonTypeCustom]; [b setBackgroundImage:[self buttonBackgroundImage] forState:UIControlStateNormal]; [b setImage:[self buttonImage] forState:UIControlStateNormal]; [b setTitle:NSLocalizedString(@"Click me!", nil) forState:UIControlStateNormal]; [b sizeToFit];
sizeToFit and end up with
The button elements are all crammed together with no spacing. This is expected, and to fix this we need to give the button some insets.
UIEdgeInsets properties that
you can play with to adjust the spacing of the elements in the
If you've ever tried adjusting these in Interface Builder, you'll
know things can get a little... interesting. For example, you may
have tried increasing
imageEdgeInsets.left 1 point at a
time and seen how the button image seems to move unpredictably,
sometimes making big steps between values:
The reason for this is that a positive inset value will shrink the layout rectangle for the image and give you you unpredictable results as the button tries to fit the image into a bounding box which is too small.
UIEdgeInsets describes what insets represent:
Edge inset values are applied to a rectangle to shrink or expand the area represented by that rectangle. Typically, edge insets are used during view layout to modify the view’s frame. Positive values cause the frame to be inset (or shrunk) by the specified amount. Negative values cause the frame to be outset (or expanded) by the specified amount.
What this means is that if we want to reliably shift the image or text, we must add or subtract equal (but opposite) amounts to both left/right or top/bottom insets.
Confused? I'm not surprised. To help you visualize all this more
easily and see the effect of
sizeToFit at the same
time, I've written a little iPhone app called
source is available on github.
Please forgive the made-by-a-programmer UI, this is purely for testing.
If you play around with this UI for a while, you should end up even
more confused than you were before. I seriously considered cracking
out IDA and diving in to the
UIButton internals to
figure out what is going on, but I really want to finish this post.
(hint to the curious: the relevant selectors are
What you need to know is the following...
contentEdgeInsets is pretty intuitive and will behave
as you expect. You can easily add space around both the image and
text to pad things out nicely. Use positive values to inset the
content. The implementation of
sizeThatFits: causes the
button to grow appropriately when we call
UPDATE: I originally wrote about how it's possible to get pixel misaligned images with certain content insets. Naturally, this turned out to be my own fault.
The golden rule when it comes to these two insets is to add equal and opposite offsets to the left and right insets. So if you add 5pt to the left title inset, you must apply -5pt to the right. This means you are using these insets only to offset the image or text, not to resize them in any way.
If you do not follow this rule, the calculated layout rect for the title (or image) may become too small and you risk text truncation and other unexpected results:
This problem may not reveal itself until you have a string of the appropriate length, so if the text in your buttons is dynamic or localization-aware you need to be careful.
The top/bottom insets do not seem to have any major issues, but you should probably follow the same rule for these as well.
UIButton's mystical insets behaviour could be the topic
of an entire blog post of its own, but I think we have enough
information to continue on our way.
Back to our button.
We want to space out the elements a little better, and make
sizeToFit does the right thing.
First we'll add some left and right content insets:
// ... UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0f, 15.0f, 0.0f, 15.0f); [b setContentEdgeInsets:contentInsets]; [b sizeToFit];
Next we want to shift the text to the right, to create some space between it and the image. Following our golden rule, we add equal but opposite amounts to the left and right insets:
// ... UIEdgeInsets titleInsets = UIEdgeInsetsMake(0.0f, 8.0f, 0.0f, -8.0f); UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0f, 15.0f, 0.0f, 15.0f); [b setTitleEdgeInsets:titleInsets]; [b setContentEdgeInsets:contentInsets]; [b sizeToFit];
Finally, we adjust the content insets again to add some extra space on the right for our inset text:
UIEdgeInsets titleInsets = UIEdgeInsetsMake(0.0f, 8.0f, 0.0f, -8.0f); UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0f, 15.0f, 0.0f, 15.0f); CGFloat extraWidthRequiredForTitle = titleInsets.left - titleInsets.right; contentInsets.right += extraWidthRequiredForTitle; [b setTitleEdgeInsets:titleInsets]; [b setContentEdgeInsets:contentInsets]; [b sizeToFit];
And at long last, we're finished. We can set arbitrarily long titles
on the button, call
sizeToFit, and we get correct
I hope this has been useful. If you can provide further insight
UIButton's layout behaviour, I'd love to hear from