228

If I have a UIButton arranged using autolayout, its size adjusts nicely to fit its content.

If I set an image as button.image, the instrinsic size again seems to account for this.

However, if I tweak the titleEdgeInsets of the button, the layout does not account for this and instead truncates the button title.

How can I ensure that the intrinsic width of the button accounts for the inset?

enter image description here

Edit:

I am using the following:

[self.backButton setTitleEdgeInsets:UIEdgeInsetsMake(0, 5, 0, 0)];

The goal is to add some separation between the image and the text.

Daniyar
  • 2,975
  • 2
  • 26
  • 39
Ben Packard
  • 26,102
  • 25
  • 102
  • 183
  • 3
    Did you file this as a radar? It certainly appears to be a bug in the UIButton's intrinsic size calculations. – Ryan Poolos Dec 10 '13 at 14:33
  • 1
    I was ready to file a radar, but this actually seems to be an expected behavior. This is documented on [UIButton's *EdgeInsets](https://developer.apple.com/library/ios/documentation/uikit/reference/UIButton_Class/UIButton/UIButton.html#//apple_ref/doc/uid/TP40006815-CH3-SW20) properties: "The insets you specify are applied to the title rectangle after that rectangle has been sized to fit the button’s text. Thus, positive inset values may actually clip the title text. [...] The button does not use this property to determine intrinsicContentSize and sizeThatFits:." – Guillaume Algis May 11 '14 at 12:51
  • 8
    @GuillaumeAlgis I would argue that although this is stated behavior, it is _not_ at all what one would expect to happen when using autolayout. I've filed a bug and would encourage others to file one as well. – memmons Oct 14 '14 at 15:01
  • If you can link to the radar bug here, can we click on it and +1 on it ? – gprasant Feb 02 '17 at 00:44
  • 1
    from `titleEdgeInset` documentation: `The insets you specify are applied to the title rectangle after that rectangle has been sized to fit the button’s text. Thus, positive inset values may actually clip the title text.` So by adding inset you are forcing the button to clip the text for sure – Marco Pappalardo Apr 09 '19 at 16:57
  • I would suggest not using the button for UI purposes a pizza approach of UIView with UIlabel and a Transparent button on top gives a lot more flexibility. – amar Nov 26 '19 at 05:09

11 Answers11

235

You can get this to work in Interface Builder (without writing any code), by using a combination of negative and positive Title and Content Insets.

enter image description here

Update: Xcode 7 has a bug where you cannot enter negative values in the Right Inset field, but you can use the stepper control next to it to decrease the value. (Thanks Stuart)

Doing this will add 8pt of spacing between the image and the title and will increase the intrinsic width of the button by the same amount. Like this:

enter image description here

n.Drake
  • 2,585
  • 2
  • 15
  • 8
  • 2
    It's using contentEdgeInsets (which is not buggy) to let autolayout to increase button width. And move the label to empty space in the right. Clever way to workaround the title edge inset bug. – ugur Aug 13 '15 at 13:24
  • 7
    This trick no longer works. Interface builder does no longer accept negative values in the `Right` field. – Joris Mans Dec 06 '15 at 10:38
  • 1
    @JorisMans that's correct. You can manually edit the storyboard as a workaround. – Daniel Hepper Dec 15 '15 at 08:54
  • 8
    @JorisMans You cannot _type_ negative values in, but it worked for me by using the stepper control to the right of the text field to step down to the required negative value... go figure! – Stuart Jan 21 '16 at 17:59
  • This is a better solution than subclassing and overriding -intrinsicContentSize because UIButton is a class cluster, which is more difficult to subclass correctly. – greenisus Mar 02 '16 at 23:28
  • 3
    This should be the first answer, why is it down here? I've tried the other 5 before finding this... – Lord Zsolt May 13 '16 at 15:37
  • 2
    I made Right of content inset 16 to center the text in UIButton – coolcool1994 Apr 28 '17 at 21:12
  • 1
    This makes no sense. And that fancy image is even worse...looks like there's left content padding but the value is 0??? BUT...it works...and we move on. – Andrew Kirna Jul 29 '19 at 16:28
  • You can no longer set a negative value on the right title inset, even with the stepper control. – Joris Mans Nov 13 '19 at 22:41
  • 1
    I confirmed that set a negative value on the right title inset is possible on Xcode v11.3.1(11C504). It worked for me. – mazend Mar 13 '20 at 12:53
205

You can solve this without having to override any methods or set an arbitrary width constraint. You can do it all in Interface Builder as follows.

  • Intrinsic button width is derived from the title width plus the icon width plus the left and right content edge insets.

  • If a button has both an image and text, they’re centered as a group, with no padding between.

  • If you add a left content inset, it’s calculated relative to the text, not the text + icon.

  • If you set a negative left image inset, the image is pulled out to the left but the overall button width is unaffected.

  • If you set a negative left image inset, the actual layout uses half that value. So to get a -20 point left inset, you must use a -40 point left inset value in Interface Builder.

So you provide a big enough left content inset to create space for both the desired left inset and the inner padding between the icon and the text, and then shift the icon left by doubling the amount of padding you want between the icon and the text. The result is a button with equal left and right content insets, and a text and icon pair that are centered as a group, with a specific amount of padding between them.

Some example values:

// Produces a button with the layout:
// |-20-icon-10-text-20-|
// AutoLayout intrinsic width works as you'd desire.
button.contentEdgeInsets = UIEdgeInsetsMake(10, 30, 10, 20)
button.imageEdgeInsets = UIEdgeInsetsMake(0, -20, 0, 0)
jaredsinclair
  • 12,687
  • 5
  • 35
  • 56
  • why the actual layout uses half of the negative left inset value?? I have encountered the same problem! – Tony Lin Jul 19 '17 at 06:15
  • 1
    It's great that there is a workaround, but I hope this isn't used to justify the weird behavior of `UIButton`. – funct7 Nov 25 '18 at 00:52
103

Why not override the intrinsicContentSize method on UIView? For example:

- (CGSize) intrinsicContentSize
{
    CGSize s = [super intrinsicContentSize];

    return CGSizeMake(s.width + self.titleEdgeInsets.left + self.titleEdgeInsets.right,
                      s.height + self.titleEdgeInsets.top + self.titleEdgeInsets.bottom);
}

This should tell the autolayout system that it should increase the size of the button to allow for the insets and show the full text. I'm not at my own computer, so I haven't tested this.

itsji10dra
  • 4,603
  • 3
  • 39
  • 59
Maarten
  • 1,873
  • 1
  • 13
  • 16
  • 1
    Buttons shouldn't be overrided as far as I know. The problem is that every button type is implemented by a different subclass. – Sulthan Jul 23 '13 at 10:57
  • 2
    `intrinsicContentSize` is a method on UIView, not UIButton, so you wouldn't be messing in any UIButton methods. Apple doesnt think it's a problem: "Overriding this method allows a custom view to communicate to the layout system what size it would like to be based on its content." And the OP didn't say anything about different buttons, just the one. – Maarten Jul 23 '13 at 11:43
  • 1
    This definitely works and is the solution I went with. ```intrinsicContentSize``` is indeed a method on UIView and UIButton is a subclass of UIView so of course you can override this method; nothing in Apple's docs says that you should not. Simply make a UIButton subclass using Maarten's overridden method and change your UIButton in Interface Builder to be of type YourUIButtonSubclass and it will work perfectly. – n8tr Jan 24 '14 at 19:23
  • 38
    Seems to me `intrinsicContentSize` for UIButton should add in the titleEdgeInsets, I'm going to file a bug with Apple. – progrmr Jul 04 '14 at 16:57
  • 6
    I agree, and the same for imageEdgeInsets. – Ricardo Sanchez-Saez Jul 24 '14 at 14:40
  • This a decent solution, but it is far from ideal to have to override UIButton. – aeskreis Mar 09 '16 at 19:45
  • 1
    Thanks for the great solution. My suggestion is: implement this as extension (Category) to get the same effect without writing the whole class – Vinh Apr 14 '16 at 12:38
96

You haven't specified how you're setting the insets, so I'm guessing that you're using titleEdgeInsets because I see the same effect you're getting. If I use contentEdgeInsets instead it works properly.

- (IBAction)ChangeTitle:(UIButton *)sender {
    self.button.contentEdgeInsets = UIEdgeInsetsMake(0,20,0,20);
    [self.button setTitle:@"Long Long Title" forState:UIControlStateNormal];
}
rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • I am indeed using titleEdgeInsets. I need to distance the title from the image, not the image from the edge of the button. Maybe I should just use an image with some padding in it? Seems hacky though. – Ben Packard Jul 23 '13 at 04:14
  • 3
    This is the better solution, as it does exactly what you want without touching intrinsicContentSize. – RyJ Oct 21 '14 at 19:33
  • 30
    This does NOT answer the question when using an image and needing to adjust the inset between the image and the title! – Brody Robertson Jul 02 '15 at 18:30
25

And for Swift worked this:

extension UIButton {
    override open var intrinsicContentSize: CGSize {
        let intrinsicContentSize = super.intrinsicContentSize

        let adjustedWidth = intrinsicContentSize.width + titleEdgeInsets.left + titleEdgeInsets.right
        let adjustedHeight = intrinsicContentSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom

        return CGSize(width: adjustedWidth, height: adjustedHeight)
    }
}

Love U Swift

M-P
  • 4,909
  • 3
  • 25
  • 31
Pau Ballada
  • 1,491
  • 14
  • 13
  • 3
    Even though you aren't supposed to, it's better to subclass in this case because Apple docs explicitly state that intrinsic size does not include titleEdgeInsets in its calculation and so by using an extension you are violating not just Apple's expectations but all other developers who read the docs. – Allison Jun 23 '19 at 04:48
  • 1
    overriding inside an extension is unsupported and results in undefined runtime behavior. see: https://stackoverflow.com/a/38274660/4175475 – Nathan Hosselton Aug 31 '20 at 22:26
18

This thread is a bit old, but I just ran into this myself and was able to solve it by using a negative inset. For example, substitute your desired padding values here:

UIButton* myButton = [[UIButton alloc] init];
// setup some autolayout constraints here
myButton.titleEdgeInsets = UIEdgeInsetsMake(-desiredBottomPadding,
                                            -desiredRightPadding,
                                            -desiredTopPadding,
                                            -desiredLeftPadding);

Combined with the right autolayout constraints, you end up with an auto-resizing button which contains an image and text! Seen below with desiredLeftPadding set to 10.

Button with image and short text

Button with image and long text

You can see that the actual frame of the button doesn't encompass the label (since the label is shifted 10 points to the right, outside the bounds), but we've achieved 10 points of padding between the text and the picture.

shim
  • 9,289
  • 12
  • 69
  • 108
Brian Gerstle
  • 3,643
  • 3
  • 22
  • 22
  • 1
    This is the solution I've used as it doesn't require subclassing. Won't work if your button has a background, but that's not usually a problem with iOS 7 – José Manuel Sánchez Jan 14 '14 at 15:51
  • This will work with a background image if you also set the content offset of the button (positive value >= title inset). – Ben Flynn Sep 04 '14 at 17:53
9

I wanted to add a 5pt space between my UIButton icon and the label. This is how I achieved it:

UIButton *infoButton = [UIButton buttonWithType:UIButtonTypeCustom];
// more button config etc
infoButton.contentEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 5);
infoButton.titleEdgeInsets = UIEdgeInsetsMake(0, 5, 0, -5);

The way contentEdgeInsets, titleEdgeInsets and imageEdgeInsets relate to each other requires a little give and take from each inset. So if you add some insets to the title's left you have to add negative inset on the right and provide some more space (via a positive inset) on the content right.

By adding a right content inset to match the shift of the title insets my text doesn't go outside the bounds of the button.

orj
  • 13,234
  • 14
  • 63
  • 73
8

For Swift 3 based on pegpeg's answer:

extension UIButton {

    override open var intrinsicContentSize: CGSize {

        let intrinsicContentSize = super.intrinsicContentSize

        let adjustedWidth = intrinsicContentSize.width + titleEdgeInsets.left + titleEdgeInsets.right
        let adjustedHeight = intrinsicContentSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom

        return CGSize(width: adjustedWidth, height: adjustedHeight)

    }

}
TheoK
  • 3,601
  • 5
  • 27
  • 37
7

All above did not work for iOS 9+, what i did is:

  • Add a width constraint (for a minimum width when the button doesn't have any text. The button will auto scale if text is provided)
  • set the relation to Greater Than or Equal

enter image description here

Now to add a border around the button just use the method:

button.contentEdgeInsets = UIEdgeInsetsMake(0,20,0,20);
Oritm
  • 2,113
  • 2
  • 25
  • 40
  • Why not? It automatically scales with the contents, you just have to set a minimum width (which can be smaller than the to be displayed text) – Oritm Dec 06 '15 at 21:48
  • Because you define a minimum width. The entire idea of autolayout is have it done without setting any explicit (minimal) width. – Joris Mans Dec 07 '15 at 10:47
  • Its not about the width, you can set the width to 1 if you prefer, but autolayout needs to know that the width can be equal or **more**. I updated my answer – Oritm Dec 07 '15 at 12:36
  • You don't need the width constraint at all, the contentEdgeInset is the key, auto layout then uses that for intrinsic content size. – Chris Conover May 09 '18 at 20:55
3

The option is also available in interface builder. See the Inset. I set left and right to 3. Works like a charm.

Interface builder screenshot

Ben Packard
  • 26,102
  • 25
  • 102
  • 183
zeiteisen
  • 7,078
  • 5
  • 50
  • 68
  • 1
    Yeah, as [this answer](http://stackoverflow.com/a/17800840/796419) explains, the reason it works is because you are adjusting **Edge: Content** here instead of **Edge: Title** or **Edge: Image**. – smileyborg Jan 20 '15 at 22:08
1

The solution I use is to add a width constraint on the button. Then somewhere in initialization, after your text is set, update the width constraint like so:

self.buttonWidthConstraint.constant = self.shareButton.intrinsicContentSize.width + 8;

Where 8 is whatever your inset is.

Bob Spryn
  • 17,742
  • 12
  • 68
  • 91
  • What is buttonWidthConstraint? – Alexey Golikov Jan 22 '14 at 10:04
  • @AlexeyGolikov An NSLayoutConstraint -- https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSLayoutConstraint_Class/NSLayoutConstraint/NSLayoutConstraint.html – 1in9ui5t Feb 13 '14 at 01:08
  • 1
    This isn't a great solution, because if the intrinsic content size of the button changes, you'd need to manually update the `constant` of the constraint to the new value...and knowing when the intrinsic content size of the button changes is difficult without subclassing the button. – smileyborg Jan 20 '15 at 22:10
  • Ayup. I don't use this method anymore. Surprised it was worthy of a down vote but ¯\_(ツ)_/¯ – Bob Spryn Jan 21 '15 at 01:43
  • A call to `setNeedsUpdateConstraints` can be "manually" made after updating the button title or image. You can then override `updateConstraints` and re-calculate `buttonWidthConstraint`'s constant from there. This is not necessarily the best approach but it works good enough for me. YMMV ;) – Olivier Dec 09 '16 at 11:45
  • Simplest solution of the lot. Perfect for a view with static text. Thanks! – Mike Critchley Jan 24 '18 at 10:45