I'm trying to create a button which has some text beneath the icon (sorta like the app buttons) however it seems to be quite difficult to achieve. Any ideas how can I go about get the text to display below the image with a UIButton
?

- 18,845
- 10
- 77
- 85

- 7,407
- 13
- 52
- 91
-
It is fairly easy and doable to make a custom subclass of UIbutton containing a UIImage and UILabel, positioned like you would need... – NP Compete Nov 17 '10 at 06:43
-
7Or just use a UIButton and UILabel. – raidfive Nov 17 '10 at 06:49
-
To precisely control with the size and auto layout, you can try this: `https://github.com/albert-zhang/AZCenterLabelButton` ([Link](https://github.com/albert-zhang/AZCenterLabelButton)) – Albert Zhang Dec 18 '14 at 02:55
-
works fine with this solution https://stackoverflow.com/a/59666154/1576134 – Shreyank Jan 09 '20 at 14:32
-
From **Xcode 13** there is option in storyboard just change the button **Placement** option to **top** that work fine for me – Reema Jan 22 '22 at 10:55
35 Answers
Or you can just use this category:
ObjC
@interface UIButton (VerticalLayout)
- (void)centerVerticallyWithPadding:(float)padding;
- (void)centerVertically;
@end
@implementation UIButton (VerticalLayout)
- (void)centerVerticallyWithPadding:(float)padding {
CGSize imageSize = self.imageView.frame.size;
CGSize titleSize = self.titleLabel.frame.size;
CGFloat totalHeight = (imageSize.height + titleSize.height + padding);
self.imageEdgeInsets = UIEdgeInsetsMake(- (totalHeight - imageSize.height),
0.0f,
0.0f,
- titleSize.width);
self.titleEdgeInsets = UIEdgeInsetsMake(0.0f,
- imageSize.width,
- (totalHeight - titleSize.height),
0.0f);
self.contentEdgeInsets = UIEdgeInsetsMake(0.0f,
0.0f,
titleSize.height,
0.0f);
}
- (void)centerVertically {
const CGFloat kDefaultPadding = 6.0f;
[self centerVerticallyWithPadding:kDefaultPadding];
}
@end
Swift extension
extension UIButton {
func centerVertically(padding: CGFloat = 6.0) {
guard
let imageViewSize = self.imageView?.frame.size,
let titleLabelSize = self.titleLabel?.frame.size else {
return
}
let totalHeight = imageViewSize.height + titleLabelSize.height + padding
self.imageEdgeInsets = UIEdgeInsets(
top: -(totalHeight - imageViewSize.height),
left: 0.0,
bottom: 0.0,
right: -titleLabelSize.width
)
self.titleEdgeInsets = UIEdgeInsets(
top: 0.0,
left: -imageViewSize.width,
bottom: -(totalHeight - titleLabelSize.height),
right: 0.0
)
self.contentEdgeInsets = UIEdgeInsets(
top: 0.0,
left: 0.0,
bottom: titleLabelSize.height,
right: 0.0
)
}
}
Suggestion:
If button height is less than totalHeight
, then image will draw outside borders.
imageEdgeInset.top
should be:
max(0, -(totalHeight - imageViewSize.height))

- 1
- 1

- 39,540
- 23
- 113
- 143
-
6I think this is the best answer since it uses edgeInsets instead of manually adjusting the frame. It works great with auto layout too when called from layoutSubviews in the button's superview. Only suggestion is to use `CGRectGetHeight()` and `CGRectGetWidth()` when getting the imageView and titleLabel height and width. – Jesse Oct 24 '14 at 18:35
-
2When I use this the image pops above the button view, to center it should I `CGFloat inset = (self.frame.size.height - totalHeight)/2; self.contentEdgeInsets = UIEdgeInsetsMake(inset, 0.0f, inset, 0.0f);` – Alex Hedley Aug 04 '17 at 10:03
-
15
-
-
The solution is not working for me. The following values are set into imageViewSize and TitleLabelSize: mageViewSize = (CGSize) (width = 0, height = 0), titleLabelSize (CGSize) (width = 0, height = 18) – Spasitel Oct 13 '19 at 15:09
-
Do you mean center "horizontally"? OP described they want the title label below the image.. which would mean they are centered horizontally, and not vertically. – vikzilla Nov 07 '19 at 00:07
-
1works fine with this solution https://stackoverflow.com/a/59666154/1576134 – Shreyank Jan 09 '20 at 14:31
-
I was able to force this to do what I needed, by commenting out the part that modified contentEdgeInsets. You do need to call this method in an override of didLayoutSubviews, or the position will be wonky (off to the right and down a bit in my case). – Oscar Mar 03 '20 at 20:17
In Xcode, you can simply set the Edge Title Left Inset to negative the width of the image. This will display the label in the center of the image.
To get the label to display below the image (sorta like the app buttons), you may need to set the Edge Title Top Inset to some positive number.
Edit: Here is some code to achieve that without using Interface Builder:
/// This will move the TitleLabel text of a UIButton to below it's Image and Centered.
/// Note: No point in calling this function before autolayout lays things out.
/// - Parameter padding: Some extra padding to be applied
func centerVertically(padding: CGFloat = 18.0) {
// No point in doing anything if we don't have an imageView size
guard let imageFrame = imageView?.frame else { return }
titleLabel?.numberOfLines = 0
titleEdgeInsets.left = -(imageFrame.width + padding)
titleEdgeInsets.top = (imageFrame.height + padding)
}
Please note this won't work if you're using autolayout and the button didn't get layed out in the screen yet via constraints.
-
1This is the way to do it... unless you're doing this repeatedly with a number of buttons (of various sizes)... in which case I had good results with a tweaked version of Erik W's solution – Kenny Winker Jul 11 '13 at 22:28
-
5Just to make sure people realize this. The value should be the negative width of the image, even if the button is wider than the width of the image. – Liron Sep 09 '13 at 12:49
-
1This did not work for me. My text still appears to the right of the image, i.e. does not wrap below it. – Erika Electra Oct 30 '13 at 12:37
-
1@Cindeselia Thats surprising. How big of a value did you use for the Top Inset? Maybe try increasing it to an even larger value? – Chris Nov 01 '13 at 01:54
-
unfortunately, there are alignment differences between ios versions with this method – Radu Simionescu Apr 04 '14 at 21:45
-
3In iOS7, it seems not work. Label only moves to bottom of image and hidden, not show anymore. – Brave Jun 06 '14 at 20:03
-
I had to use negative the button's frame width, rather than negative the button's image frame width. – Chucky Jul 24 '17 at 12:29
-
this doesn't work. with the insets the placement is totally different on the device/simulator and don't match up to that in IB – Hogdotmac Sep 12 '17 at 09:29
-
The problem I have is, the image ends up left-aligned. This is bad when the button ends up being wider than the image... – Nicolas Miari Apr 03 '18 at 01:14
-
I used the edge insets method proposed by Chris, but since the button width was smaller than the total width of both the image and the title of the button, the title showed as 3 dots (...). I couldn't increase the width of the button due to layout reasons. I solved this by changing the Line Break attribute to: "Clip". That makes the title go down beneath the image. Then I corrected its location using the insets. This works only when the above problem exists. – Shaked Sayag May 27 '15 at 15:59
-
why `negative the width of the image` will make the text to the middle ? – zionpi Sep 13 '20 at 03:23
-
@zionpi pulling the label on the x-axis in negative direction will bring the label towards left. – C0D3 Sep 28 '20 at 15:25
-
I think this should be the selected answer as other answers with a UIButton extension centerVertically() function didn't work for me. I briefly tried subclassing but that seems like a lot of work for something simple I wanted to achieve. Changing the titleEdgeInsets.left and top seems to work! – C0D3 Sep 28 '20 at 15:27
This is a simple centered title button implemented in Swift by overriding titleRect(forContentRect:)
and imageRect(forContentRect:)
. It also implements intrinsicContentSize
for use with AutoLayout.
import UIKit
class CenteredButton: UIButton
{
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.titleRect(forContentRect: contentRect)
return CGRect(x: 0, y: contentRect.height - rect.height + 5,
width: contentRect.width, height: rect.height)
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.imageRect(forContentRect: contentRect)
let titleRect = self.titleRect(forContentRect: contentRect)
return CGRect(x: contentRect.width/2.0 - rect.width/2.0,
y: (contentRect.height - titleRect.height)/2.0 - rect.height/2.0,
width: rect.width, height: rect.height)
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
if let image = imageView?.image {
var labelHeight: CGFloat = 0.0
if let size = titleLabel?.sizeThatFits(CGSize(width: self.contentRect(forBounds: self.bounds).width, height: CGFloat.greatestFiniteMagnitude)) {
labelHeight = size.height
}
return CGSize(width: size.width, height: image.size.height + labelHeight + 5)
}
return size
}
override init(frame: CGRect) {
super.init(frame: frame)
centerTitleLabel()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
centerTitleLabel()
}
private func centerTitleLabel() {
self.titleLabel?.textAlignment = .center
}
}

- 4,466
- 2
- 38
- 42
-
8That is the most correct solution. But some modification needed for intrinsic content size. It should return MAX width between image and label: return CGSizeMake(MAX(labelSize.width, self.imageView.image.size.width), self.imageView.image.size.height + labelHeight) – kirander Nov 26 '15 at 09:59
-
Look at this great answer in Swift.
extension UIButton {
func alignImageAndTitleVertically(padding: CGFloat = 6.0) {
let imageSize = self.imageView!.frame.size
let titleSize = self.titleLabel!.frame.size
let totalHeight = imageSize.height + titleSize.height + padding
self.imageEdgeInsets = UIEdgeInsets(
top: -(totalHeight - imageSize.height),
left: 0,
bottom: 0,
right: -titleSize.width
)
self.titleEdgeInsets = UIEdgeInsets(
top: 0,
left: -imageSize.width,
bottom: -(totalHeight - titleSize.height),
right: 0
)
}
}

- 75,165
- 16
- 143
- 189

- 3,113
- 2
- 31
- 45
-
4If you also want the image centered vertically, replace `left` in `imageEdgeInsets` with `(self.frame.size.width - imageSize.width) / 2` – elsurudo Sep 20 '16 at 08:21
-
If you are using autolayout call this method in `layoutSubviews()` of your superview. – AlexVogel Aug 16 '19 at 07:31
-
Who is setting the `imageView`'s frame? Wouldn't it be better if you used `imageView?.image.size`? – Zorayr Aug 17 '20 at 19:56
Subclass UIButton
. Override -layoutSubviews
to move the built-in subviews
into new positions:
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect frame = self.imageView.frame;
frame = CGRectMake(truncf((self.bounds.size.width - frame.size.width) / 2), 0.0f, frame.size.width, frame.size.height);
self.imageView.frame = frame;
frame = self.titleLabel.frame;
frame = CGRectMake(truncf((self.bounds.size.width - frame.size.width) / 2), self.bounds.size.height - frame.size.height, frame.size.width, frame.size.height);
self.titleLabel.frame = frame;
}

- 1,977
- 3
- 14
- 30

- 8,795
- 1
- 46
- 50
-
I personally had to set the titleLabel y value to 0 and the height to the frame height for it to display the text with the image. It doesn't make sense to me but it works... though I'm still learning the 'Apple' way of setting up controls. – Russ Mar 07 '12 at 18:31
-
7Actually, the better way is to override `titleRectForContentRect` and `imageRectForContentRect` – Mazyod Aug 04 '15 at 21:11
Refactored icecrystal23`s answer.
Swift 3, works with autolayouts, xib, storyboards, can be animated.
Button in orginal icecrystal23`s answer had a badly calculated frame. I think I fixed that.
Edit: Updated to Swift 5 and made work inside Interface Builder / Storyboards
Edit: Updated to support localization (ltr/rtl) per Alexsander Akers' comment on the answer below
import UIKit
@IBDesignable
class VerticalButton: UIButton {
@IBInspectable public var padding: CGFloat = 20.0 {
didSet {
setNeedsLayout()
}
}
override var intrinsicContentSize: CGSize {
let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
if let titleSize = titleLabel?.sizeThatFits(maxSize), let imageSize = imageView?.sizeThatFits(maxSize) {
let width = ceil(max(imageSize.width, titleSize.width))
let height = ceil(imageSize.height + titleSize.height + padding)
return CGSize(width: width, height: height)
}
return super.intrinsicContentSize
}
override func layoutSubviews() {
if let image = imageView?.image, let title = titleLabel?.attributedText {
let imageSize = image.size
let titleSize = title.size()
if effectiveUserInterfaceLayoutDirection == .leftToRight {
titleEdgeInsets = UIEdgeInsets(top: 0.0, left: -imageSize.width, bottom: -(imageSize.height + padding), right: 0.0)
imageEdgeInsets = UIEdgeInsets(top: -(titleSize.height + padding), left: 0.0, bottom: 0.0, right: -titleSize.width)
}
else {
titleEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: -(imageSize.height + padding), right: -imageSize.width)
imageEdgeInsets = UIEdgeInsets(top: -(titleSize.height + padding), left: -titleSize.width, bottom: 0.0, right: 0.0)
}
}
super.layoutSubviews()
}
}

- 2,639
- 3
- 21
- 32

- 4,684
- 5
- 32
- 58
-
2There is an issue with this when the image is removed. I am using an image for selected state and no image for default state. When the state is changed from selected to default the label is messed up. So a few fixes are needed: Do not check image view but use 'image(for: state)'. Set zero edge insets when there is no image in else statement of layoutSubviews. – Matic Oblak Sep 18 '17 at 12:16
-
Only solution here that works. Other answers seem to work, but actually the button bounds don't resize according to label and image size. Set a background color to see this. – Manuel Nov 27 '18 at 19:47
-
This solution didn't work, I think it caused some sort of an infinite loop and eventually Xcode crash. I removed the intrinsicContentSize part and it worked fine (Xcode 11.5) – mojuba Jul 09 '20 at 13:31
-
And you can support your `contentEdgeInsets` by computing CGSize as follows: `CGSize(width: width + contentEdgeInsets.left + contentEdgeInsets.right, height: height + contentEdgeInsets.top + contentEdgeInsets.bottom)` – androidguy Oct 20 '20 at 01:13
corrected one of the answers here:
Swift 3:
class CenteredButton: UIButton
{
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.titleRect(forContentRect: contentRect)
let imageRect = super.imageRect(forContentRect: contentRect)
return CGRect(x: 0, y: imageRect.maxY + 10,
width: contentRect.width, height: rect.height)
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.imageRect(forContentRect: contentRect)
let titleRect = self.titleRect(forContentRect: contentRect)
return CGRect(x: contentRect.width/2.0 - rect.width/2.0,
y: (contentRect.height - titleRect.height)/2.0 - rect.height/2.0,
width: rect.width, height: rect.height)
}
override init(frame: CGRect) {
super.init(frame: frame)
centerTitleLabel()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
centerTitleLabel()
}
private func centerTitleLabel() {
self.titleLabel?.textAlignment = .center
}
}

- 3,549
- 1
- 27
- 32
Dave's solution in Swift:
override func layoutSubviews() {
super.layoutSubviews()
if let imageView = self.imageView {
imageView.frame.origin.x = (self.bounds.size.width - imageView.frame.size.width) / 2.0
imageView.frame.origin.y = 0.0
}
if let titleLabel = self.titleLabel {
titleLabel.frame.origin.x = (self.bounds.size.width - titleLabel.frame.size.width) / 2.0
titleLabel.frame.origin.y = self.bounds.size.height - titleLabel.frame.size.height
}
}

- 1,130
- 1
- 9
- 17
-
Good answer. Add @IBDesignable to your subclass and see it in the storyboard. – Joel Teply Jun 07 '16 at 04:27
If you subclass UIButton
and override layoutSubviews
, you can use the below to center the image and place the title centered below it:
kTextTopPadding
is a constant you'll have to introduce that determines the space between the image and the text below it.
-(void)layoutSubviews {
[super layoutSubviews];
// Move the image to the top and center it horizontally
CGRect imageFrame = self.imageView.frame;
imageFrame.origin.y = 0;
imageFrame.origin.x = (self.frame.size.width / 2) - (imageFrame.size.width / 2);
self.imageView.frame = imageFrame;
// Adjust the label size to fit the text, and move it below the image
CGRect titleLabelFrame = self.titleLabel.frame;
CGSize labelSize = [self.titleLabel.text sizeWithFont:self.titleLabel.font
constrainedToSize:CGSizeMake(self.frame.size.width, CGFLOAT_MAX)
lineBreakMode:NSLineBreakByWordWrapping];
titleLabelFrame.size.width = labelSize.width;
titleLabelFrame.size.height = labelSize.height;
titleLabelFrame.origin.x = (self.frame.size.width / 2) - (labelSize.width / 2);
titleLabelFrame.origin.y = self.imageView.frame.origin.y + self.imageView.frame.size.height + kTextTopPadding;
self.titleLabel.frame = titleLabelFrame;
}

- 1,977
- 3
- 14
- 30

- 807
- 9
- 25
This is a modified version of Erik W's excellent answer. But instead of placing the image centered at the TOP of the view, it places the image and the label centred in the view as a group.
The difference is:
+-----------+
| ( ) |
| Hello | // Erik W's code
| |
| |
+-----------+
vs
+-----------+
| |
| ( ) | // My modified version
| Hello |
| |
+-----------+
Source below:
-(void)layoutSubviews {
[super layoutSubviews];
CGRect titleLabelFrame = self.titleLabel.frame;
CGSize labelSize = [self.titleLabel.text sizeWithFont:self.titleLabel.font constrainedToSize:CGSizeMake(self.frame.size.width, CGFLOAT_MAX) lineBreakMode:NSLineBreakByWordWrapping];
CGRect imageFrame = self.imageView.frame;
CGSize fitBoxSize = (CGSize){.height = labelSize.height + kTextTopPadding + imageFrame.size.height, .width = MAX(imageFrame.size.width, labelSize.width)};
CGRect fitBoxRect = CGRectInset(self.bounds, (self.bounds.size.width - fitBoxSize.width)/2, (self.bounds.size.height - fitBoxSize.height)/2);
imageFrame.origin.y = fitBoxRect.origin.y;
imageFrame.origin.x = CGRectGetMidX(fitBoxRect) - (imageFrame.size.width/2);
self.imageView.frame = imageFrame;
// Adjust the label size to fit the text, and move it below the image
titleLabelFrame.size.width = labelSize.width;
titleLabelFrame.size.height = labelSize.height;
titleLabelFrame.origin.x = (self.frame.size.width / 2) - (labelSize.width / 2);
titleLabelFrame.origin.y = fitBoxRect.origin.y + imageFrame.size.height + kTextTopPadding;
self.titleLabel.frame = titleLabelFrame;
}
FYI: This can break when combined with UIView animations, as layoutSubviews is called during them.

- 11,919
- 7
- 56
- 78
-
Shouldn't the line calculating the labelSize use self.bounds.size.width instead of self.frame.size.width? – Jeremy Wiebe May 13 '14 at 15:35
On iOS 11/ Swift 4 none of the above answers really worked for me. I found some examples and put my spin on it:
extension UIButton {
func centerImageAndButton(_ gap: CGFloat, imageOnTop: Bool) {
guard let imageView = self.currentImage,
let titleLabel = self.titleLabel?.text else { return }
let sign: CGFloat = imageOnTop ? 1 : -1
self.titleEdgeInsets = UIEdgeInsetsMake((imageView.size.height + gap) * sign, -imageView.size.width, 0, 0);
let titleSize = titleLabel.size(withAttributes:[NSAttributedStringKey.font: self.titleLabel!.font!])
self.imageEdgeInsets = UIEdgeInsetsMake(-(titleSize.height + gap) * sign, 0, 0, -titleSize.width)
}
}
Hope this helps someone.

- 141
- 1
- 2
-
Thanks for suggesting that Roman, though there is an issue where the contentEdgeInsets don't include the title and image entirely. – Patrick Aug 09 '18 at 11:25
-
Updated Kenny Winker's answer since sizeWithFont was deprecated in iOS 7.
-(void)layoutSubviews {
[super layoutSubviews];
int kTextTopPadding = 3;
CGRect titleLabelFrame = self.titleLabel.frame;
CGRect labelSize = [self.titleLabel.text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGRectGetHeight(self.bounds)) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:self.titleLabel.font} context:nil];
CGRect imageFrame = self.imageView.frame;
CGSize fitBoxSize = (CGSize){.height = labelSize.size.height + kTextTopPadding + imageFrame.size.height, .width = MAX(imageFrame.size.width, labelSize.size.width)};
CGRect fitBoxRect = CGRectInset(self.bounds, (self.bounds.size.width - fitBoxSize.width)/2, (self.bounds.size.height - fitBoxSize.height)/2);
imageFrame.origin.y = fitBoxRect.origin.y;
imageFrame.origin.x = CGRectGetMidX(fitBoxRect) - (imageFrame.size.width/2);
self.imageView.frame = imageFrame;
// Adjust the label size to fit the text, and move it below the image
titleLabelFrame.size.width = labelSize.size.width;
titleLabelFrame.size.height = labelSize.size.height;
titleLabelFrame.origin.x = (self.frame.size.width / 2) - (labelSize.size.width / 2);
titleLabelFrame.origin.y = fitBoxRect.origin.y + imageFrame.size.height + kTextTopPadding;
self.titleLabel.frame = titleLabelFrame;
}

- 1,108
- 1
- 11
- 12
-
Since pre iOS 7 is getting more and more outdated, this should be the new accepted answer. – Mehlyfication Aug 21 '15 at 07:07
Swift 5 - below method works for me
func centerVerticallyWithPadding(padding : CGFloat) {
guard
let imageViewSize = self.imageView?.frame.size,
let titleLabelSize = self.titleLabel?.frame.size else {
return
}
let totalHeight = imageViewSize.height + titleLabelSize.height + padding
self.imageEdgeInsets = UIEdgeInsets(
top: max(0, -(totalHeight - imageViewSize.height)),
left: 0.0,
bottom: 0.0,
right: -titleLabelSize.width
)
self.titleEdgeInsets = UIEdgeInsets(
top: (totalHeight - imageViewSize.height),
left: -imageViewSize.width,
bottom: -(totalHeight - titleLabelSize.height),
right: 0.0
)
self.contentEdgeInsets = UIEdgeInsets(
top: 0.0,
left: 0.0,
bottom: titleLabelSize.height,
right: 0.0
)
}
Make sure your button title is not truncate in storyboard/xib else go for
Solution 2
class SVVerticalButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
let padding : CGFloat = 2.0
if let imageView = self.imageView {
imageView.frame.origin.x = (self.bounds.size.width - imageView.frame.size.width) / 2.0
imageView.frame.origin.y = max(0,(self.bounds.size.height - (imageView.frame.size.height + (titleLabel?.frame.size.height ?? 0.0) + padding)) / 2.0)
}
if let titleLabel = self.titleLabel {
titleLabel.frame.origin.x = 0
titleLabel.frame.origin.y = self.bounds.size.height - titleLabel.frame.size.height
titleLabel.frame.size.width = self.bounds.size.width
titleLabel.textAlignment = .center
}
}
}

- 1,549
- 13
- 24
-
The top is still cut off with this solution, the size is smaller than expected. – Zorayr Aug 18 '20 at 01:07
Swift 5, AutoLayout
I implemented combination of some approaches and this works best for me. Trick is in overriding titleRect(forContentRect:)
, imageRect(forContentRect:)
methods and intrinsicContentSize
getter with calculated values of those views.
Result:
final class CustomButton: UIButton {
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
let superRect = super.titleRect(forContentRect: contentRect)
return CGRect(
x: 0,
y: contentRect.height - superRect.height,
width: contentRect.width,
height: superRect.height
)
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
let superRect = super.imageRect(forContentRect: contentRect)
return CGRect(
x: contentRect.width / 2 - superRect.width / 2,
y: (contentRect.height - titleRect(forContentRect: contentRect).height) / 2 - superRect.height / 2,
width: superRect.width,
height: superRect.height
)
}
override var intrinsicContentSize: CGSize {
_ = super.intrinsicContentSize
guard let image = imageView?.image else { return super.intrinsicContentSize }
let size = titleLabel?.sizeThatFits(contentRect(forBounds: bounds).size) ?? .zero
let spacing: CGFloat = 12
return CGSize(width: max(size.width, image.size.width), height: image.size.height + size.height + spacing)
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
titleLabel?.textAlignment = .center
}
}
The solution above may not work in iOS 12 and older. In this case create constants with fixed image size and label height depending on font size.
final class CustomButton: UIButton {
private enum Constants {
static let imageSize: CGFloat = 48
static let titleHeight = yourFont.pointSize - yourFont.descender
}
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
_ = super.titleRect(forContentRect: contentRect)
return CGRect(
x: 0,
y: contentRect.height - Constants.titleHeight,
width: contentRect.width,
height: Constants.titleHeight
)
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
return CGRect(
x: contentRect.width / 2 - Constants.imageSize / 2,
y: (contentRect.height - titleRect(forContentRect: contentRect).height) / 2 - Constants.imageSize / 2,
width: Constants.imageSize,
height: Constants.imageSize
)
}
override var intrinsicContentSize: CGSize {
_ = super.intrinsicContentSize
let size = titleLabel?.sizeThatFits(contentRect(forBounds: bounds).size) ?? .zero
let spacing: CGFloat = 12
return CGSize(
width: max(size.width, Constants.imageSize),
height: Constants.imageSize + Constants.titleHeight + spacing
)
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
titleLabel?.textAlignment = .center
}
}

- 10,580
- 2
- 22
- 40
-
Think you're missing the style == .postEditorTypeOption extension from somewhere. – Peter Suwara Dec 28 '20 at 11:19
-
1@PeterSuwara thank you, you are right. It just shouldn't be there – Robert Dresler Dec 29 '20 at 19:07
Using the code of Kenny Winker's and simeon i make this swift code that works for me.
import UIKit
@IBDesignable
class TopIconButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
let kTextTopPadding:CGFloat = 3.0;
var titleLabelFrame = self.titleLabel!.frame;
let labelSize = titleLabel!.sizeThatFits(CGSizeMake(CGRectGetWidth(self.contentRectForBounds(self.bounds)), CGFloat.max))
var imageFrame = self.imageView!.frame;
let fitBoxSize = CGSizeMake(max(imageFrame.size.width, labelSize.width), labelSize.height + kTextTopPadding + imageFrame.size. height)
let fitBoxRect = CGRectInset(self.bounds, (self.bounds.size.width - fitBoxSize.width)/2, (self.bounds.size.height - fitBoxSize. height)/2);
imageFrame.origin.y = fitBoxRect.origin.y;
imageFrame.origin.x = CGRectGetMidX(fitBoxRect) - (imageFrame.size.width/2);
self.imageView!.frame = imageFrame;
// Adjust the label size to fit the text, and move it below the image
titleLabelFrame.size.width = labelSize.width;
titleLabelFrame.size.height = labelSize.height;
titleLabelFrame.origin.x = (self.frame.size.width / 2) - (labelSize.width / 2);
titleLabelFrame.origin.y = fitBoxRect.origin.y + imageFrame.size.height + kTextTopPadding;
self.titleLabel!.frame = titleLabelFrame;
self.titleLabel!.textAlignment = .Center
}
}

- 93
- 2
- 4
@Tiago I change your answer like this. Its works fine with all sizes
func alignImageAndTitleVertically(padding: CGFloat = 5.0) {
self.sizeToFit()
let imageSize = self.imageView!.frame.size
let titleSize = self.titleLabel!.frame.size
let totalHeight = imageSize.height + titleSize.height + padding
self.imageEdgeInsets = UIEdgeInsets(
top: -(totalHeight - imageSize.height),
left: 0,
bottom: 0,
right: -titleSize.width
)
self.titleEdgeInsets = UIEdgeInsets(
top: 0,
left: 0,
bottom: -(totalHeight - titleSize.height),
right: titleSize.height
)
}

- 1,031
- 1
- 11
- 20
Localization Friendly Solution:
So many great solutions guys, but I'd like to add a note here for those who use localization.
You need to reverse the left and right EdgeInstets values to get the button laid out correctly in case of a change of language direction from LtR to RtL.
Using a similar solution I'd implement it as follows:
extension UIButton {
func alignVertical(spacing: CGFloat, lang: String) {
guard let imageSize = self.imageView?.image?.size,
let text = self.titleLabel?.text,
let font = self.titleLabel?.font
else { return }
let labelString = NSString(string: text)
let titleSize = labelString.size(
withAttributes: [NSAttributedString.Key.font: font]
)
var titleLeftInset: CGFloat = -imageSize.width
var titleRigtInset: CGFloat = 0.0
var imageLeftInset: CGFloat = 0.0
var imageRightInset: CGFloat = -titleSize.width
if Locale.current.languageCode! != "en" { // If not Left to Right language
titleLeftInset = 0.0
titleRigtInset = -imageSize.width
imageLeftInset = -titleSize.width
imageRightInset = 0.0
}
self.titleEdgeInsets = UIEdgeInsets(
top: 0.0,
left: titleLeftInset,
bottom: -(imageSize.height + spacing),
right: titleRigtInset
)
self.imageEdgeInsets = UIEdgeInsets(
top: -(titleSize.height + spacing),
left: imageLeftInset,
bottom: 0.0,
right: imageRightInset
)
let edgeOffset = abs(titleSize.height - imageSize.height) / 2.0;
self.contentEdgeInsets = UIEdgeInsets(
top: edgeOffset,
left: 0.0,
bottom: edgeOffset,
right: 0.0
)
}
}

- 1,444
- 20
- 24
-
3There are many non-English LTR languages. You are better off checking the effectiveUserInterfaceLayoutDirection on the button. – Alexsander Akers Jul 09 '19 at 18:16
-
With a large title, it is creating an issue. Any solution for that? – Maulik Pandya Jan 08 '21 at 12:32
You just have to adjust all three edge insets based on the size of your image and title label:
button.contentEdgeInsets = UIEdgeInsetsMake(0, 0, titleLabelBounds.height + 4, 0)
button.titleEdgeInsets = UIEdgeInsetsMake(image.size.height + 8, -image.size.width, 0, 0)
button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, -titleLabelBounds.width)
You can get the title label bounds by calling sizeToFit after setting its text. The horizontal spacing should work regardless of the size of the text, font and image but I don't know of a single solution to get the vertical spacing and bottom content edge inset consistent.

- 654
- 4
- 10
Here is "Bear With Me"s answer as a subclass in Swift 2.0
. To use it just change your button class in Interface Builder
to VerticalButton
and it will magically update the preview.
I also updated it to calculate the correct intrinsic content size for autolayout.
import UIKit
@IBDesignable
class VerticalButton: UIButton {
@IBInspectable var padding: CGFloat = 8
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
update()
}
override func layoutSubviews() {
super.layoutSubviews()
update()
}
func update() {
let imageBounds = self.imageView!.bounds
let titleBounds = self.titleLabel!.bounds
let totalHeight = CGRectGetHeight(imageBounds) + padding + CGRectGetHeight(titleBounds)
self.imageEdgeInsets = UIEdgeInsets(
top: -(totalHeight - CGRectGetHeight(imageBounds)),
left: 0,
bottom: 0,
right: -CGRectGetWidth(titleBounds)
)
self.titleEdgeInsets = UIEdgeInsets(
top: 0,
left: -CGRectGetWidth(imageBounds),
bottom: -(totalHeight - CGRectGetHeight(titleBounds)),
right: 0
)
}
override func intrinsicContentSize() -> CGSize {
let imageBounds = self.imageView!.bounds
let titleBounds = self.titleLabel!.bounds
let width = CGRectGetWidth(imageBounds) > CGRectGetWidth(titleBounds) ? CGRectGetWidth(imageBounds) : CGRectGetWidth(titleBounds)
let height = CGRectGetHeight(imageBounds) + padding + CGRectGetHeight(titleBounds)
return CGSizeMake(width, height)
}
}

- 11,139
- 8
- 52
- 68
-
2Ends up as an infinite loop where `layoutSubviews()` is called repeatedly in my case: `intrinsicContentSize` accesses `imageView` which makes `layoutSubviews` being called which accesses `imageView` etc. – ctietze May 16 '16 at 07:44
I took a combination of the answers here and came up with one that seems to be working for me, in Swift. I don't love how I just overrode the insets, but it works. I'd be open to suggested improvements in the comments. It seems to work correctly with sizeToFit()
and with auto layout.
import UIKit
/// A button that displays an image centered above the title. This implementation
/// only works when both an image and title are set, and ignores
/// any changes you make to edge insets.
class CenteredButton: UIButton
{
let padding: CGFloat = 0.0
override func layoutSubviews() {
if imageView?.image != nil && titleLabel?.text != nil {
let imageSize: CGSize = imageView!.image!.size
titleEdgeInsets = UIEdgeInsetsMake(0.0, -imageSize.width, -(imageSize.height + padding), 0.0)
let labelString = NSString(string: titleLabel!.text!)
let titleSize = labelString.sizeWithAttributes([NSFontAttributeName: titleLabel!.font])
imageEdgeInsets = UIEdgeInsetsMake(-(titleSize.height + padding), 0.0, 0.0, -titleSize.width)
let edgeOffset = abs(titleSize.height - imageSize.height) / 2.0;
contentEdgeInsets = UIEdgeInsetsMake(edgeOffset, 0.0, edgeOffset, 0.0)
}
super.layoutSubviews()
}
override func sizeThatFits(size: CGSize) -> CGSize {
let defaultSize = super.sizeThatFits(size)
if let titleSize = titleLabel?.sizeThatFits(size),
let imageSize = imageView?.sizeThatFits(size) {
return CGSize(width: ceil(max(imageSize.width, titleSize.width)), height: ceil(imageSize.height + titleSize.height + padding))
}
return defaultSize
}
override func intrinsicContentSize() -> CGSize {
let size = sizeThatFits(CGSize(width: CGFloat.max, height: CGFloat.max))
return size
}
}

- 331
- 3
- 10
Use this two methods:
func titleRect(forContentRect contentRect: CGRect) -> CGRect
func imageRect(forContentRect contentRect: CGRect) -> CGRect
Example:
class VerticalButton: UIButton {
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
let titleRect = super.titleRect(forContentRect: contentRect)
let imageRect = super.imageRect(forContentRect: contentRect)
return CGRect(x: 0,
y: contentRect.height - (contentRect.height - padding - imageRect.size.height - titleRect.size.height) / 2 - titleRect.size.height,
width: contentRect.width,
height: titleRect.height)
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
let imageRect = super.imageRect(forContentRect: contentRect)
let titleRect = self.titleRect(forContentRect: contentRect)
return CGRect(x: contentRect.width/2.0 - imageRect.width/2.0,
y: (contentRect.height - padding - imageRect.size.height - titleRect.size.height) / 2,
width: imageRect.width,
height: imageRect.height)
}
private let padding: CGFloat
init(padding: CGFloat) {
self.padding = padding
super.init(frame: .zero)
self.titleLabel?.textAlignment = .center
}
required init?(coder aDecoder: NSCoder) { fatalError() }
}
extension UIButton {
static func vertical(padding: CGFloat) -> UIButton {
return VerticalButton(padding: padding)
}
}
And you can use:
let myButton = UIButton.vertical(padding: 6)

- 2,363
- 1
- 19
- 17
That's definitely is an overkill for this question, however... In one of my projects I first had to implement a button with icon aligned leftmost. Then we got another button with title under image. I searched for an existing solution but with no luck So, here goes the alignable button:
@IBDesignable
class AlignableButton: UIButton {
override class var requiresConstraintBasedLayout: Bool {
return true
}
@objc enum IconAlignment: Int {
case top, left, right, bottom
}
// MARK: - Designables
@IBInspectable var iconAlignmentValue: Int {
set {
iconAlignment = IconAlignment(rawValue: newValue) ?? .left
}
get {
return iconAlignment.rawValue
}
}
var iconAlignment: IconAlignment = .left
@IBInspectable var titleAlignmentValue: Int {
set {
titleAlignment = NSTextAlignment(rawValue: newValue) ?? .left
}
get {
return titleAlignment.rawValue
}
}
var titleAlignment: NSTextAlignment = .left
// MARK: - Corner Radius
@IBInspectable
var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.masksToBounds = (newValue != 0)
layer.cornerRadius = newValue
}
}
// MARK: - Content size
override var intrinsicContentSize: CGSize {
switch iconAlignment {
case .top, .bottom:
return verticalAlignedIntrinsicContentSize
default:
return super.intrinsicContentSize
}
}
private var verticalAlignedIntrinsicContentSize: CGSize {
if let imageSize = imageView?.intrinsicContentSize,
let labelSize = titleLabel?.intrinsicContentSize {
let width = max(imageSize.width, labelSize.width) + contentEdgeInsets.left + contentEdgeInsets.right
let height = imageSize.height + labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom
return CGSize(
width: ceil(width),
height: ceil(height)
)
}
return super.intrinsicContentSize
}
// MARK: - Image Rect
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
switch iconAlignment {
case .top:
return topAlignedImageRect(forContentRect: contentRect)
case .bottom:
return bottomAlignedImageRect(forContentRect: contentRect)
case .left:
return leftAlignedImageRect(forContentRect: contentRect)
case .right:
return rightAlignedImageRect(forContentRect: contentRect)
}
}
func topAlignedImageRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.imageRect(forContentRect: contentRect)
let x = (contentRect.width - rect.width) / 2.0 + contentRect.minX
let y = contentRect.minY
let w = rect.width
let h = rect.height
return CGRect(
x: x,
y: y,
width: w,
height: h
).inset(by: imageEdgeInsets)
}
func bottomAlignedImageRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.imageRect(forContentRect: contentRect)
let x = (contentRect.width - rect.width) / 2.0 + contentRect.minX
let y = contentRect.height - rect.height + contentRect.minY
let w = rect.width
let h = rect.height
return CGRect(
x: x,
y: y,
width: w,
height: h
).inset(by: imageEdgeInsets)
}
func leftAlignedImageRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.imageRect(forContentRect: contentRect)
let x = contentRect.minX
let y = (contentRect.height - rect.height) / 2 + contentRect.minY
let w = rect.width
let h = rect.height
return CGRect(
x: x,
y: y,
width: w,
height: h
).inset(by: imageEdgeInsets)
}
func rightAlignedImageRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.imageRect(forContentRect: contentRect)
let x = (contentRect.width - rect.width) + contentRect.minX
let y = (contentRect.height - rect.height) / 2 + contentRect.minY
let w = rect.width
let h = rect.height
return CGRect(
x: x,
y: y,
width: w,
height: h
).inset(by: imageEdgeInsets)
}
// MARK: - Title Rect
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
switch iconAlignment {
case .top:
return topAlignedTitleRect(forContentRect: contentRect)
case .bottom:
return bottomAlignedTitleRect(forContentRect: contentRect)
case .left:
return leftAlignedTitleRect(forContentRect: contentRect)
case .right:
return rightAlignedTitleRect(forContentRect: contentRect)
}
}
func topAlignedTitleRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.titleRect(forContentRect: contentRect)
let x = contentRect.minX
let y = contentRect.height - rect.height + contentRect.minY
let w = contentRect.width
let h = rect.height
return CGRect(
x: x,
y: y,
width: w,
height: h
).inset(by: titleEdgeInsets)
}
func bottomAlignedTitleRect(forContentRect contentRect: CGRect) -> CGRect {
let rect = super.titleRect(forContentRect: contentRect)
let x = contentRect.minX
let y = contentRect.minY
let w = contentRect.width
let h = rect.height
return CGRect(
x: x,
y: y,
width: w,
height: h
).inset(by: titleEdgeInsets)
}
func leftAlignedTitleRect(forContentRect contentRect: CGRect) -> CGRect {
let titleRect = super.titleRect(forContentRect: contentRect)
let imageRect = self.imageRect(forContentRect: contentRect)
let x = imageRect.width + imageRect.minX
let y = (contentRect.height - titleRect.height) / 2.0 + contentRect.minY
let w = contentRect.width - imageRect.width * 2.0
let h = titleRect.height
return CGRect(
x: x,
y: y,
width: w,
height: h
).inset(by: titleEdgeInsets)
}
func rightAlignedTitleRect(forContentRect contentRect: CGRect) -> CGRect {
let titleRect = super.titleRect(forContentRect: contentRect)
let imageRect = self.imageRect(forContentRect: contentRect)
let x = contentRect.minX + imageRect.width
let y = (contentRect.height - titleRect.height) / 2.0 + contentRect.minY
let w = contentRect.width - imageRect.width * 2.0
let h = titleRect.height
return CGRect(
x: x,
y: y,
width: w,
height: h
).inset(by: titleEdgeInsets)
}
// MARK: - Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
titleLabel?.textAlignment = titleAlignment
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
titleLabel?.textAlignment = titleAlignment
}
}
Hope you find it useful.

- 611
- 1
- 8
- 20
Using @Robert Dresler solution wasn't working for me on IOS 15, so I extended the button class by adding a condition to check if is IOS 15 and use the new ui button configuration for IOS 15
final class ImageButton: UIButton {
private enum Constants {
static let imageSize: CGFloat = 40
static let titleHeight: CGFloat = 12
}
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
if #available(iOS 15, *) {
return super.titleRect(forContentRect: contentRect)
}
else {
_ = super.titleRect(forContentRect: contentRect)
return CGRect(
x: 0,
y: contentRect.height - Constants.titleHeight,
width: contentRect.width,
height: Constants.titleHeight
)
}
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
if #available(iOS 15, *) {
return super.imageRect(forContentRect: contentRect)
} else {
return CGRect(
x: contentRect.width / 2 - Constants.imageSize / 2,
y: (contentRect.height - titleRect(forContentRect: contentRect).height) / 2 - Constants.imageSize / 2,
width: Constants.imageSize,
height: Constants.imageSize
)
}
}
override var intrinsicContentSize: CGSize {
if #available(iOS 15, *) {
return super.intrinsicContentSize
}
else {
_ = super.intrinsicContentSize
let size = titleLabel?.sizeThatFits(contentRect(forBounds: bounds).size) ?? .zero
let spacing: CGFloat = 12
return CGSize(
width: max(size.width, Constants.imageSize),
height: Constants.imageSize + Constants.titleHeight + spacing
)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
if #available(iOS 15, *) {
var myConfiguration = UIButton.Configuration.plain()
myConfiguration.imagePlacement = .top
self.configuration = myConfiguration
} else {
titleLabel?.textAlignment = .center
}
}
}
This solution works on IOS 12 to 15
Here's a snapshot of the final result

- 312
- 3
- 15

- 2,415
- 2
- 23
- 24
-
2Thanks, this was great. Note, I added the inline var spacing to Constants, as this was also relevant for iOS 15 code: myConfiguration.imagePadding = Constants.spacing – horseshoe7 Feb 25 '22 at 15:58
I think one of the best ways to do that is by subclassing UIButton and override some rendering methods:
- (void)awakeFromNib
{
[super awakeFromNib];
[self setupSubViews];
}
- (instancetype)init
{
if (self = [super init])
{
[self setupSubViews];
}
return self;
}
- (void)setupSubViews
{
[self.imageView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.imageView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
[self.titleLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView][titleLabel]|" options:NSLayoutFormatAlignAllCenterX metrics:nil views:@{@"imageView": self.imageView, @"titleLabel": self.titleLabel}]];
}
- (CGSize)intrinsicContentSize
{
CGSize imageSize = self.imageView.image.size;
CGSize titleSize = [self.titleLabel.text sizeWithAttributes:@{NSFontAttributeName: self.titleLabel.font}];
return CGSizeMake(MAX(imageSize.width, titleSize.width), imageSize.height + titleSize.height);
}

- 1,808
- 2
- 20
- 30
I found that Simeon's answer was probably the best but it was giving me strange results on some buttons and I just couldn't work out why. So using his answer as a base I implemented my buttons as per below:
#define PADDING 2.0f
@implementation OOButtonVerticalImageText
-(CGSize) intrinsicContentSize {
CGSize size = [super intrinsicContentSize];
CGFloat labelHeight = 0.0f;
CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake([self contentRectForBounds:self.bounds].size.width, CGFLOAT_MAX)];
labelHeight = titleSize.height;
return CGSizeMake(MAX(titleSize.width, self.imageView.image.size.width), self.imageView.image.size.height + labelHeight + PADDING);
}
-(void) layoutSubviews {
[super layoutSubviews];
CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake([self contentRectForBounds:self.bounds].size.width, CGFLOAT_MAX)];
self.titleLabel.frame = CGRectMake((self.bounds.size.width - titleSize.width)/2.0f,
self.bounds.size.height - titleSize.height - PADDING,
titleSize.width,
titleSize.height);
CGSize ivSize = self.imageView.frame.size;
self.imageView.frame = CGRectMake((self.bounds.size.width - ivSize.width)/2.0f,
self.titleLabel.frame.origin.y - ivSize.height - PADDING,
ivSize.width,
ivSize.height);
}
@end

- 363
- 3
- 9
Here is my subclass of UIButton
which solves this problem:
@implementation MyVerticalButton
@synthesize titleAtBottom; // BOOL property
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.titleAtBottom = YES;
}
return self;
}
- (CGSize)sizeThatFits:(CGSize)size {
self.titleLabel.text = [self titleForState: self.state];
UIEdgeInsets imageInsets = self.imageEdgeInsets;
UIEdgeInsets titleInsets = self.titleEdgeInsets;
CGSize imageSize = [self imageForState: self.state].size;
if (!CGSizeEqualToSize(imageSize, CGSizeZero)) {
imageSize.width += imageInsets.left + imageInsets.right;
imageSize.height += imageInsets.top + imageInsets.bottom;
}
CGSize textSize = [self.titleLabel sizeThatFits: CGSizeMake(size.width - titleInsets.left - titleInsets.right,
size.height -(imageSize.width +
titleInsets.top+titleInsets.bottom))];
if (!CGSizeEqualToSize(textSize, CGSizeZero)) {
textSize.width += titleInsets.left + titleInsets.right;
textSize.height += titleInsets.top + titleInsets.bottom;
}
CGSize result = CGSizeMake(MAX(textSize.width, imageSize.width),
textSize.height + imageSize.height);
return result;
}
- (void)layoutSubviews {
// needed to update all properities of child views:
[super layoutSubviews];
CGRect bounds = self.bounds;
CGRect titleFrame = UIEdgeInsetsInsetRect(bounds, self.titleEdgeInsets);
CGRect imageFrame = UIEdgeInsetsInsetRect(bounds, self.imageEdgeInsets);
if (self.titleAtBottom) {
CGFloat titleHeight = [self.titleLabel sizeThatFits: titleFrame.size].height;
titleFrame.origin.y = CGRectGetMaxY(titleFrame)-titleHeight;
titleFrame.size.height = titleHeight;
titleFrame = CGRectStandardize(titleFrame);
self.titleLabel.frame = titleFrame;
CGFloat imageBottom = CGRectGetMinY(titleFrame)-(self.titleEdgeInsets.top+self.imageEdgeInsets.bottom);
imageFrame.size.height = imageBottom - CGRectGetMinY(imageFrame);
self.imageView.frame = CGRectStandardize(imageFrame);
} else {
CGFloat titleHeight = [self.titleLabel sizeThatFits: titleFrame.size].height;
titleFrame.size.height = titleHeight;
titleFrame = CGRectStandardize(titleFrame);
self.titleLabel.frame = titleFrame;
CGFloat imageTop = CGRectGetMaxY(titleFrame)+(self.titleEdgeInsets.bottom+self.imageEdgeInsets.top);
imageFrame.size.height = CGRectGetMaxY(imageFrame) - imageTop;
self.imageView.frame = CGRectStandardize(imageFrame);
}
}
- (void)setTitleAtBottom:(BOOL)newTitleAtBottom {
if (titleAtBottom!=newTitleAtBottom) {
titleAtBottom=newTitleAtBottom;
[self setNeedsLayout];
}
}
@end
That's it. Works like charm. Problem may appear if button will be to small to fit title and text.

- 32,568
- 6
- 55
- 140
-
It does work like a charm indeed! And with auto layout too. Thanks a lot for sharing this solution. I was going nuts with this and resorting to create my own UIControl subclass. – valeCocoa Sep 25 '17 at 19:42
@simeon's solution in Objective-C
#import "CenteredButton.h"
@implementation CenteredButton
- (CGRect)titleRectForContentRect:(CGRect)contentRect
{
CGRect rect = [super titleRectForContentRect: contentRect];
return CGRectMake(0,
contentRect.size.height - rect.size.height - 5,
contentRect.size.width,
rect.size.height);
}
- (CGRect)imageRectForContentRect:(CGRect)contentRect
{
CGRect rect = [super imageRectForContentRect: contentRect];
CGRect titleRect = [self titleRectForContentRect: contentRect];
return CGRectMake(contentRect.size.width / 2.0 - rect.size.width / 2.0,
(contentRect.size.height - titleRect.size.height)/2.0 - rect.size.height/2.0,
rect.size.width,
rect.size.height);
}
- (CGSize)intrinsicContentSize {
CGSize imageSize = [super intrinsicContentSize];
if (self.imageView.image) {
UIImage* image = self.imageView.image;
CGFloat labelHeight = 0.0;
CGSize labelSize = [self.titleLabel sizeThatFits: CGSizeMake([self contentRectForBounds: self.bounds].size.width, CGFLOAT_MAX)];
if (CGSizeEqualToSize(imageSize, labelSize)) {
labelHeight = imageSize.height;
}
return CGSizeMake(MAX(labelSize.width, imageSize.width), image.size.height + labelHeight + 5);
}
return imageSize;
}
- (id) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self centerTitleLabel];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self centerTitleLabel];
}
return self;
}
- (void)centerTitleLabel {
self.titleLabel.textAlignment = NSTextAlignmentCenter;
}
@end

- 426
- 4
- 7
-
I think the intrinsicContentSize is not correct here. I don't understand what the part with the CGSizeEqualToSize is for but you only have a label size > 0 if the label size matches the intrinsicContentSize of UILabel. It should be sufficient to just return `CGSizeMake(MAX(labelSize.width, image.size.width), image.size.height + labelSize.height + 5.0)` in the if-case – Oliver Jun 28 '17 at 06:32
If you are using custom fonts the calculation for the titleLabel size won't work properly, you should replace it with
let titleLabelSize = self.titleLabel?.text?.size(withAttributes: [NSAttributedStringKey.font: self.titleLabel!.font!])

- 984
- 12
- 26
Instead of going through hell trying to position the icon and the text with edge insets, you could create a NSAttributedString with your image as an attachment and set it to your button's attributed title instead:
let titleText = NSAttributedString(string: yourTitle, attributes: attributes)
let imageAttachment = NSTextAttachment()
imageAttachment.image = yourImage
let title = NSMutableAttributedString(string: "")
title.append(NSAttributedString(attachment: imageAttachment))
title.append(titleText)
button.setAttributedTitle(title, for: .normal)

- 327
- 3
- 10
-
Doesn't work for the OPs question where the text should be centered *below* the image. A `UIButton`'s text field layouts to display only 1 line, hence it doesn't work even when using a line break in the attributed string. Would be a nice solution otherwise. – Manuel Nov 27 '18 at 19:11
-
It is also important to set `button.titleLabel?.numberOfLines` in order to get the needed number of lines – swearwolf Nov 28 '18 at 20:09
Something like this inside UIButton
subclass
public override func layoutSubviews() {
super.layoutSubviews()
imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 0, 0)
titleEdgeInsets = UIEdgeInsetsMake(0, -bounds.size.width/2 - 10, -30, 0)
}

- 45,645
- 31
- 257
- 263
iOS 11 - Objective-C
-(void)layoutSubviews {
[super layoutSubviews];
CGRect titleLabelFrame = self.titleLabel.frame;
CGSize labelSize = [self.titleLabel.text sizeWithAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:12.0f]}];
CGSize adjustedLabelSize = CGSizeMake(ceilf(labelSize.width), ceilf(labelSize.height));
CGRect imageFrame = self.imageView.frame;
CGSize fitBoxSize = (CGSize){.height = adjustedLabelSize.height + kTextTopPadding + imageFrame.size.height, .width = MAX(imageFrame.size.width, adjustedLabelSize.width)};
CGRect fitBoxRect = CGRectInset(self.bounds, (self.bounds.size.width - fitBoxSize.width)/2, (self.bounds.size.height - fitBoxSize.height)/2);
imageFrame.origin.y = fitBoxRect.origin.y;
imageFrame.origin.x = CGRectGetMidX(fitBoxRect) - (imageFrame.size.width/2);
self.imageView.frame = imageFrame;
// Adjust the label size to fit the text, and move it below the image
titleLabelFrame.size.width = adjustedLabelSize.width;
titleLabelFrame.size.height = adjustedLabelSize.height;
titleLabelFrame.origin.x = (self.frame.size.width / 2) - (adjustedLabelSize.width / 2);
titleLabelFrame.origin.y = fitBoxRect.origin.y + imageFrame.size.height + kTextTopPadding;
self.titleLabel.frame = titleLabelFrame; }

- 1,304
- 8
- 27
Top image and bottom title button with subclassing UIButton
class VerticalButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
let padding: CGFloat = 8
let iH = imageView?.frame.height ?? 0
let tH = titleLabel?.frame.height ?? 0
let v: CGFloat = (frame.height - iH - tH - padding) / 2
if let iv = imageView {
let x = (frame.width - iv.frame.width) / 2
iv.frame.origin.y = v
iv.frame.origin.x = x
}
if let tl = titleLabel {
let x = (frame.width - tl.frame.width) / 2
tl.frame.origin.y = frame.height - tl.frame.height - v
tl.frame.origin.x = x
}
}
}

- 519
- 8
- 11
Another way I found:
UIButton has 2 UIImageViews. First of them we can reach using UIButton's imageView property. Another UIImage view is inaccessible via UIButton's properties, but that another UIImageView perfectly fits to implement centered button. To access that background image view I added an extension:
extension UIButton {
var backgroundImageView: UIImageView? {
return subviews.first(where: { $0.isKind(of: UIImageView.self) && $0 != imageView }) as? UIImageView
}
}
The next step is to setup the button:
centeredButton.setBackgroundImage(perfectImage, for: .normal) // This sets image to Button's backgroundImageView
centeredButton.backgroundImageView?.contentMode = .top // This moves image to the top of the Button's frame
centeredButton.contentVerticalAlignment = .bottom // This moves label to the bottom of the Button's frame.
The next step is to set padding between image and label. You can make this by adding height constraint to the button. The image will alway be on the top, label on the bottom.

- 2,963
- 2
- 21
- 39
Storyboard Friendly Solution
Here's a way to do it which allows insets to be further adjusted in the storyboard:
@IBDesignable
class RotatableButton: UIButton {
@IBInspectable public var rotate: Bool = false {
didSet { if (rotate) { Rotate() } }
}
func Rotate() {
self.titleLabel?.transform = CGAffineTransform(rotationAngle: CGFloat(-CGFloat.pi/2))
self.transform = CGAffineTransform(rotationAngle: CGFloat(CGFloat.pi/2))
// Hacky workaround text clipping
self.titleLabel?.lineBreakMode = .byWordWrapping
self.setTitle((self.titleLabel?.text)! + "\n\n", for: .normal)
}
}

- 1
Its pretty simple.
Instead of this:
button.setImage(UIImage(named: "image"), forState: .Normal)
Use this:
button.setBackgroundImage(UIImage(named: "image", forState: .Normal)
Then you can add text on the button easily using:
// button.titleLabel!.font = UIFont(name: "FontName", size: 30)
button.setTitle("TitleText", forState: UIControlState.Normal)

- 199
- 1
- 8