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):
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:
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?
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)
}
}
}