Label under image in UIButton

110,560

Solution 1

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))

Solution 2

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.

Solution 3

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
    }
}

Solution 4

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

}

Solution 5

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;
}
Share:
110,560

Related videos on Youtube

NRaf
Author by

NRaf

Updated on November 28, 2021

Comments

  • NRaf
    NRaf over 2 years

    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?

    • NP Compete
      NP Compete over 13 years
      It is fairly easy and doable to make a custom subclass of UIbutton containing a UIImage and UILabel, positioned like you would need...
    • raidfive
      raidfive over 13 years
      Or just use a UIButton and UILabel.
    • Albert Zhang
      Albert Zhang over 9 years
      To precisely control with the size and auto layout, you can try this: https://github.com/albert-zhang/AZCenterLabelButton (Link)
    • Shreyank
      Shreyank over 4 years
      works fine with this solution stackoverflow.com/a/59666154/1576134
    • Reema
      Reema over 2 years
      From Xcode 13 there is option in storyboard just change the button Placement option to top that work fine for me
  • Russ
    Russ about 12 years
    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.
  • Kenny Winker
    Kenny Winker almost 11 years
    This 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
  • Liron
    Liron over 10 years
    Just 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.
  • Erika Electra
    Erika Electra over 10 years
    This did not work for me. My text still appears to the right of the image, i.e. does not wrap below it.
  • Chris
    Chris over 10 years
    @Cindeselia Thats surprising. How big of a value did you use for the Top Inset? Maybe try increasing it to an even larger value?
  • Radu Simionescu
    Radu Simionescu about 10 years
    unfortunately, there are alignment differences between ios versions with this method
  • Jeremy Wiebe
    Jeremy Wiebe about 10 years
    Shouldn't the line calculating the labelSize use self.bounds.size.width instead of self.frame.size.width?
  • Brave
    Brave almost 10 years
    In iOS7, it seems not work. Label only moves to bottom of image and hidden, not show anymore.
  • Jesse
    Jesse over 9 years
    I 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.
  • Shaked Sayag
    Shaked Sayag almost 9 years
    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.
  • Mazyod
    Mazyod almost 9 years
    Actually, the better way is to override titleRectForContentRect and imageRectForContentRect
  • Mehlyfication
    Mehlyfication over 8 years
    Since pre iOS 7 is getting more and more outdated, this should be the new accepted answer.
  • kirander
    kirander over 8 years
    That 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)
  • ctietze
    ctietze about 8 years
    Ends 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.
  • Joel Teply
    Joel Teply almost 8 years
    Good answer. Add @IBDesignable to your subclass and see it in the storyboard.
  • elsurudo
    elsurudo over 7 years
    If you also want the image centered vertically, replace left in imageEdgeInsets with (self.frame.size.width - imageSize.width) / 2
  • Oliver
    Oliver almost 7 years
    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
  • Chucky
    Chucky almost 7 years
    I had to use negative the button's frame width, rather than negative the button's image frame width.
  • Alex Hedley
    Alex Hedley almost 7 years
    When 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);
  • Hogdotmac
    Hogdotmac over 6 years
    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
  • Matic Oblak
    Matic Oblak over 6 years
    There 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.
  • valeCocoa
    valeCocoa over 6 years
    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.
  • Nicolas Miari
    Nicolas Miari about 6 years
    The problem I have is, the image ends up left-aligned. This is bad when the button ends up being wider than the image...
  • Patrick
    Patrick almost 6 years
    Thanks for suggesting that Roman, though there is an issue where the contentEdgeInsets don't include the title and image entirely.
  • Patrick
    Patrick almost 6 years
    The Swift extension did not layout it out correctly for me.
  • Argus
    Argus over 5 years
    It work if Image was set as setImage, not as setBackgroundImage.
  • Manuel
    Manuel over 5 years
    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
    Manuel over 5 years
    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.
  • swearwolf
    swearwolf over 5 years
    It is also important to set button.titleLabel?.numberOfLines in order to get the needed number of lines
  • Alexsander Akers
    Alexsander Akers almost 5 years
    There are many non-English LTR languages. You are better off checking the effectiveUserInterfaceLayoutDirection on the button.
  • AlexVogel
    AlexVogel almost 5 years
    If you are using autolayout call this method in layoutSubviews() of your superview.
  • Spasitel
    Spasitel over 4 years
    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)
  • vikzilla
    vikzilla over 4 years
    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.
  • Shreyank
    Shreyank over 4 years
    works fine with this solution stackoverflow.com/a/59666154/1576134
  • Oscar
    Oscar about 4 years
    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).
  • mojuba
    mojuba almost 4 years
    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)
  • Zorayr
    Zorayr almost 4 years
    Who is setting the imageView's frame? Wouldn't it be better if you used imageView?.image.size?
  • Zorayr
    Zorayr over 3 years
    The top is still cut off 😢 with this solution, the size is smaller than expected.
  • zionpi
    zionpi over 3 years
    why negative the width of the image will make the text to the middle ?
  • C0D3
    C0D3 over 3 years
    @zionpi pulling the label on the x-axis in negative direction will bring the label towards left.
  • C0D3
    C0D3 over 3 years
    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!
  • androidguy
    androidguy over 3 years
    And you can support your contentEdgeInsets by computing CGSize as follows: CGSize(width: width + contentEdgeInsets.left + contentEdgeInsets.right, height: height + contentEdgeInsets.top + contentEdgeInsets.bottom)
  • Peter Suwara
    Peter Suwara over 3 years
    Think you're missing the style == .postEditorTypeOption extension from somewhere.
  • Robert Dresler
    Robert Dresler over 3 years
    @PeterSuwara thank you, you are right. It just shouldn't be there
  • Maulik Pandya
    Maulik Pandya over 3 years
    With a large title, it is creating an issue. Any solution for that?
  • horseshoe7
    horseshoe7 about 2 years
    Thanks, 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
  • Cory
    Cory about 2 years
    In my opinion, best answer so far