Ios – Aligning text and image on UIButton with imageEdgeInsets and titleEdgeInsets

iosiphoneuibuttonuikit

I would like to place an icon left of the two lines of text such that there's about 2-3 pixels of space between the image and the start of text. The control itself is Center aligned horizontally (set through Interface Builder)

The button would resemble something like this:

|                  |
|[Image] Add To    |
|        Favorites |

I'm trying to configure this with contentEdgeInset, imageEdgeInsets and titleEdgeInsets to no avail. I understand that a negative value expands the edge while a positive value shrinks it to move it closer to the center.

I tried:

[button setTitleEdgeInsets:UIEdgeInsetsMake(0, -image.size.width, 0, 0)];
[button setImageEdgeInsets:UIEdgeInsetsMake(0, button.titleLabel.bounds.size.width, 0, 0)];

but this doesn't display it correctly. I've been tweaking the values but going from say -5 to -10 on the left inset value doesn't appear to move it in expected manner. -10 will scoot the text all the way to the left so I expected -5 to scoot it half way from the left side but it doesn't.

What's the logic behind insets? I'm not familiar with image placements and related terminology.

I used this SO question as a reference but something about my values isn't right.
UIButton: how to center an image and a text using imageEdgeInsets and titleEdgeInsets?

Best Answer

I'm a little late to this party, but I think I have something useful to add.

Kekoa's answer is great but, as RonLugge mentions, it can make the button no longer respect sizeToFit or, more importantly, can cause the button to clip its content when it is intrinsically sized. Yikes!

First, though,

A brief explanation of how I believe imageEdgeInsets and titleEdgeInsets work:

The docs for imageEdgeInsets have the following to say, in part:

Use this property to resize and reposition the effective drawing rectangle for the button image. You can specify a different value for each of the four insets (top, left, bottom, right). A positive value shrinks, or insets, that edge—moving it closer to the center of the button. A negative value expands, or outsets, that edge.

I believe that this documentation was written imagining that the button has no title, just an image. It makes a lot more sense thought of this way, and behaves how UIEdgeInsets usually do. Basically, the frame of the image (or the title, with titleEdgeInsets) is moved inwards for positive insets and outwards for negative insets.

OK, so what?

I'm getting there! Here's what you have by default, setting an image and a title (the button border is green just to show where it is):

Starting image; no space between title and image.

When you want spacing between an image and a title, without causing either to be crushed, you need to set four different insets, two on each of the image and title. That's because you don't want to change the sizes of those elements' frames, but just their positions. When you start thinking this way, the needed change to Kekoa's excellent category becomes clear:

@implementation UIButton(ImageTitleCentering)

- (void)centerButtonAndImageWithSpacing:(CGFloat)spacing {
    CGFloat insetAmount = spacing / 2.0;
    self.imageEdgeInsets = UIEdgeInsetsMake(0, -insetAmount, 0, insetAmount);
    self.titleEdgeInsets = UIEdgeInsetsMake(0, insetAmount, 0, -insetAmount);
}

@end

But wait, you say, when I do that, I get this:

Spacing is good, but the image and title are outside the view's frame.

Oh yeah! I forgot, the docs warned me about this. They say, in part:

This property is used only for positioning the image during layout. The button does not use this property to determine intrinsicContentSize and sizeThatFits:.

But there is a property that can help, and that's contentEdgeInsets. The docs for that say, in part:

The button uses this property to determine intrinsicContentSize and sizeThatFits:.

That sounds good. So let's tweak the category once more:

@implementation UIButton(ImageTitleCentering)

- (void)centerButtonAndImageWithSpacing:(CGFloat)spacing {
    CGFloat insetAmount = spacing / 2.0;
    self.imageEdgeInsets = UIEdgeInsetsMake(0, -insetAmount, 0, insetAmount);
    self.titleEdgeInsets = UIEdgeInsetsMake(0, insetAmount, 0, -insetAmount);
    self.contentEdgeInsets = UIEdgeInsetsMake(0, insetAmount, 0, insetAmount);
}

@end

And what do you get?

Spacing and frame are now correct.

Looks like a winner to me.


Working in Swift and don't want to do any thinking at all? Here's the final version of the extension in Swift:

extension UIButton {

    func centerTextAndImage(spacing: CGFloat) {
        let insetAmount = spacing / 2
        let isRTL = UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) == .rightToLeft
        if isRTL {
            previewButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: insetAmount, bottom: 0, right: -insetAmount)
            previewButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: -insetAmount, bottom: 0, right: insetAmount)
            previewButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: -insetAmount, bottom: 0, right: -insetAmount)
        } else {
            previewButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: -insetAmount, bottom: 0, right: insetAmount)
            previewButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: insetAmount, bottom: 0, right: -insetAmount)
            previewButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: insetAmount, bottom: 0, right: insetAmount)
        }
    }
}
Related Topic