124

I have a UIButton with text "Explore the app" and UIImage (>) In Interface Builder it looks like:

[ (>) Explore the app ]

But I need to place this UIImage AFTER the text:

[ Explore the app (>) ]

How can I move the UIImage to the right?

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
Pavel Yakimenko
  • 3,234
  • 4
  • 28
  • 33
  • You can use Interface Builder too. Check out my answer here: http://stackoverflow.com/questions/2765024/how-to-set-the-title-as-left-alignment-in-the-uibutton/22725345#22725345 – n8tr Mar 29 '14 at 00:27
  • 1
    Just use this subclass with a few rows of code: https://github.com/k06a/RTLButton – k06a May 26 '15 at 15:43

16 Answers16

164

My solution to this is quite simple

[button sizeToFit];
button.titleEdgeInsets = UIEdgeInsetsMake(0, -button.imageView.frame.size.width, 0, button.imageView.frame.size.width);
button.imageEdgeInsets = UIEdgeInsetsMake(0, button.titleLabel.frame.size.width, 0, -button.titleLabel.frame.size.width);
Split
  • 4,319
  • 4
  • 19
  • 10
  • 4
    This works great! It's hard to understand the edge inset concept. Any idea why we need to set both left and right edge inset? Theoretically, if I move the title to left and the image to right, that would be enough. Why do I need to set both left and right? – PokerIncome.com Jul 11 '13 at 18:23
  • 3
    THE BEST SOLUTION I'VE FOUND – Bimawa Sep 24 '13 at 04:02
  • 2
    This answer works perfectly. The accepted answer is correct, and points to the correct API docs, but this is the copy and paste solution to do what the OP requested. – mclaughj Mar 24 '14 at 16:55
  • Becomes a bit complicated when you start changing the button text. The subclassed solution worked better for me in this case. – Brad G Mar 18 '15 at 15:52
  • Thank you for showing me you have to apply equal widths to left and right sides, I don't 100% understand how it works (because sometimes it will affect the size as well as origin) but I've been able to solve similar issues using this method. – Toby Dec 19 '15 at 03:34
138

On iOS 9 onwards, seems that a simple way to achieve this is to force the semantic of the view.

enter image description here

Or programmatically, using:

button.semanticContentAttribute = .ForceRightToLeft
Alvivi
  • 3,263
  • 3
  • 26
  • 28
  • 4
    As much as i liked your answer (+1), I hate to say it might not be the "proper" way to do this, but it's one of the easiest. – farzadshbfn Jun 25 '16 at 15:40
  • @farzadshbfn you are right. I changed that word for "simple", seems more consistent. – Alvivi Jun 27 '16 at 06:25
  • @farzadshbfn Why would this not be the "proper" way to do? – Samman Bikram Thapa Apr 04 '19 at 17:26
  • 3
    @SammanBikramThapa IMHO the proper way would be to subclass the UIButton and override layoutSubviews and respect `semanticContentAttribute` in our layout logic, instead of changing `semanticContentAttribute` itself. (changing semantic approach, will not work well with internationalization) – farzadshbfn Apr 07 '19 at 05:17
  • @farzadshbfn is totally right. No matter this answer scores most points it's not correct - It may break UX for right to left interface. – Pavel Yakimenko Nov 15 '19 at 10:11
  • "right-to-left"l is used for "right-to-left" languages like Arabic. So it is a bad approach to use it for something else. – Vyachaslav Gerchicov May 12 '21 at 13:55
  • @Alvivi it do have top button too ? – kiran Aug 18 '21 at 00:22
86

Set the imageEdgeInset and titleEdgeInset to move the components around within your image. You could also create a button using those graphics that is full size, and use that as the background image for the button (then use titleEdgeInsets to move the title around).

Steven Canfield
  • 7,312
  • 5
  • 35
  • 28
  • 8
    It is much less code to simply set the insets than to implement a subclass. This is the whole point of the insets. Manipulating frames for sub views (that you have not created) feels more like a hack. – Kim Feb 20 '13 at 15:25
  • 6
    @kimsnarf, really? It's a lot less work (and *less* of a hack) to tweak the insets whenever you make a minor change in the size of the image or the length of the title? – Kirk Woll Aug 21 '13 at 22:36
54

Raymond W's answer is best here. Subclass UIButton with custom layoutSubviews. Extremely simple to do, here's a layoutSubviews implementation that worked for me:

- (void)layoutSubviews
{
    // Allow default layout, then adjust image and label positions
    [super layoutSubviews];

    UIImageView *imageView = [self imageView];
    UILabel *label = [self titleLabel];

    CGRect imageFrame = imageView.frame;
    CGRect labelFrame = label.frame;

    labelFrame.origin.x = imageFrame.origin.x;
    imageFrame.origin.x = labelFrame.origin.x + CGRectGetWidth(labelFrame);

    imageView.frame = imageFrame;
    label.frame = labelFrame;
}
Chris Miles
  • 7,346
  • 2
  • 37
  • 34
  • 2
    This way is better in the case you need to manage many buttons, but I need to change only one button :) – Pavel Yakimenko Oct 06 '11 at 14:01
  • 2
    If the button image is nil the label results misplaced, probably because the UIImageView is not inserted (Tested on iOS6.0). You should consider editing frames only if imageView.image is not nil. – Scakko Apr 17 '13 at 15:19
  • 2
    I would suggest the following improvement to this answer so both views stay centered: 'CGFloat cumulativeWidth = CGRectGetWidth(imageFrame) + CGRectGetWidth(labelFrame) + 10; CGFloat excessiveWidth = CGRectGetWidth(self.bounds) - cumulativeWidth; labelFrame.origin.x = excessiveWidth / 2; imageFrame.origin.x = CGRectGetMaxX(labelFrame) + 10;' – i-konov Feb 17 '14 at 14:02
  • This breaks on iOS 7 for me. Anyone else? Works fine on iOS 8. – rounak May 22 '15 at 12:28
  • 1
    Don't support iOS7 at all and your problem will be gone. You shouldn't supoprt it anyway. – Gil Sand Oct 28 '15 at 14:52
30

What about subclassing UIButton and overriding layoutSubviews?

Then post-processing the locations of self.imageView & self.titleLabel

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
Raymond W
  • 555
  • 4
  • 5
  • 4
    This is so much easier than all the other advice (like the one above) about trying to place everything correctly using hand-tuned insets. – Steven Kramer Jun 29 '11 at 12:54
14

Another simple way (that is NOT iOS 9 only) is to subclass UIButton to override these two methods

override func titleRectForContentRect(contentRect: CGRect) -> CGRect {
    var rect = super.titleRectForContentRect(contentRect)
    rect.origin.x = 0
    return rect
}

override func imageRectForContentRect(contentRect: CGRect) -> CGRect {
    var rect = super.imageRectForContentRect(contentRect)
    rect.origin.x = CGRectGetMaxX(contentRect) - CGRectGetWidth(rect)
    return rect
}

contentEdgeInsets is already taken into account by using super.

DrAL3X
  • 769
  • 8
  • 18
10

Forcing 'right-to-left' for the button is not an option if your app supports both 'left-to-right' and 'right-to-left'.

The solution that worked for me is a subclass that can be added to the button in the Storyboard and works well with constraints (tested in iOS 11):

class ButtonWithImageAtEnd: UIButton {

    override func layoutSubviews() {
        super.layoutSubviews()

        if let imageView = imageView, let titleLabel = titleLabel {
            let padding: CGFloat = 15
            imageEdgeInsets = UIEdgeInsets(top: 5, left: titleLabel.frame.size.width+padding, bottom: 5, right: -titleLabel.frame.size.width-padding)
            titleEdgeInsets = UIEdgeInsets(top: 0, left: -imageView.frame.width, bottom: 0, right: imageView.frame.width)
        }

    }

}

Where 'padding' would be the space between the title and the image.

Luisma
  • 109
  • 1
  • 3
  • 2
    Of course `.forceRightToLeft` is an option! Just use the opposite value (`.forceLeftToRight`) if `UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft`. – manmal Jul 06 '18 at 08:29
5

In Swift:

override func layoutSubviews(){
    super.layoutSubviews()

    let inset: CGFloat = 5

    if var imageFrame = self.imageView?.frame,
       var labelFrame = self.titleLabel?.frame {

       let cumulativeWidth = imageFrame.width + labelFrame.width + inset
       let excessiveWidth = self.bounds.width - cumulativeWidth
       labelFrame.origin.x = excessiveWidth / 2
       imageFrame.origin.x = labelFrame.origin.x + labelFrame.width + inset

       self.imageView?.frame = imageFrame
       self.titleLabel?.frame = labelFrame  
    }
}
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
ChikabuZ
  • 10,031
  • 5
  • 63
  • 86
4

Building off the answer by @split...

The answer is fantastic, but it ignores the fact that the button may have custom image and title edge insets that are set beforehand (e.g. in storyboard).

For instance, you may want the image have some padding from the top and bottom of the container, but still move the image to the right side of the button.

I extended the concept with this method:-

- (void) moveImageToRightSide {
    [self sizeToFit];

    CGFloat titleWidth = self.titleLabel.frame.size.width;
    CGFloat imageWidth = self.imageView.frame.size.width;
    CGFloat gapWidth = self.frame.size.width - titleWidth - imageWidth;
    self.titleEdgeInsets = UIEdgeInsetsMake(self.titleEdgeInsets.top,
                                            -imageWidth + self.titleEdgeInsets.left,
                                            self.titleEdgeInsets.bottom,
                                            imageWidth - self.titleEdgeInsets.right);

    self.imageEdgeInsets = UIEdgeInsetsMake(self.imageEdgeInsets.top,
                                            titleWidth + self.imageEdgeInsets.left + gapWidth,
                                            self.imageEdgeInsets.bottom,
                                            -titleWidth + self.imageEdgeInsets.right - gapWidth);
}
BFar
  • 2,447
  • 22
  • 23
2
// Get the size of the text and image
CGSize buttonLabelSize = [[self.button titleForState:UIControlStateNormal] sizeWithFont:self.button.titleLabel.font];
CGSize buttonImageSize = [[self.button imageForState:UIControlStateNormal] size];

// You can do this line in the xib too:
self.button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;

// Adjust Edge Insets according to the above measurement. The +2 adds a little space 
self.button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, -(buttonLabelSize.width+2));
self.button.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0, buttonImageSize.width+2);

This creates a right-aligned button, like so:

[           button label (>)]

The button doesn't adjust it's width according to the context, so space will appear on the left of the label. You could solve this by calculating the button's frame width from the buttonLabelSize.width and the buttonImageSize.width.

caliss
  • 29
  • 1
  • 3
1
button.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
1

Building on previous answers. If you want to have a margin between the icon and the title of the button, the code has to change a little to prevent floating of the label and icon above the bounds of intrinsically sized buttons.

let margin = CGFloat(4.0)
button.titleEdgeInsets = UIEdgeInsetsMake(0, -button.imageView.frame.size.width, 0, button.imageView.frame.size.width);
button.imageEdgeInsets = UIEdgeInsetsMake(0, button.titleLabel.frame.size.width, 0, -button.titleLabel.frame.size.width)
button.contentEdgeInsets = UIEdgeInsetsMake(0, margin, 0, margin)

The last code line is important for the intrinsically content size calculation for auto layout.

kiecodes
  • 1,642
  • 14
  • 28
1

Single line solution in Swift :

// iOS 9 and Onwards
button.semanticContentAttribute = .forceRightToLeft
Sunil Targe
  • 7,251
  • 5
  • 49
  • 80
  • It is not good as it will break the layout for all RTL users. You will need to force left to right for them. You better try other solution from here. – Pavel Yakimenko Jul 28 '20 at 11:47
0

This solution works iOS 7 and above

Just subclass UIButton

@interface UIButton (Image)

- (void)swapTextWithImage;

@end

@implementation UIButton (Image)

- (void)swapTextWithImage {
   const CGFloat kDefaultPadding = 6.0f;
   CGSize buttonSize = [self.titleLabel.text sizeWithAttributes:@{
                                                               NSFontAttributeName:self.titleLabel.font
                                                               }];

   self.titleEdgeInsets = UIEdgeInsetsMake(0, -self.imageView.frame.size.width, 0, self.imageView.frame.size.width);
   self.imageEdgeInsets = UIEdgeInsetsMake(0, buttonSize.width + kDefaultPadding, 0, -buttonSize.width); 
}

@end

Usage (Somewhere in your class):

[self.myButton setTitle:@"Any text" forState:UIControlStateNormal];
[self.myButton swapTextWithImage];
0

Here is my own way to do the thing, (after about 10 years)

  1. Subclass from UIButton (Button, as we're living in Swift era)
  2. Put an image and a label in a stack view.
class CustomButton: Button {

    var didLayout: Bool = false // The code must be called only once

    override func layoutSubviews() {
        super.layoutSubviews()
        if !didLayout, let imageView = imageView, let titleLabel = titleLabel {
            didLayout = true
            let stack = UIStackView(arrangedSubviews: [titleLabel, imageView])
            addSubview(stack)
            stack.edgesToSuperview() // I use TinyConstraints library. You could handle the constraints directly
            stack.axis = .horizontal
        }
    }
}
Pavel Yakimenko
  • 3,234
  • 4
  • 28
  • 33
0

I tried the solution, and works, but it centers the title + image. On my approach I need the text centered on navigation bar and the image on the right side.

I implemented this Custom View:

class CenteredViewWithImage: UIView {
    
    // MARK: - Vars
    private let marginBetweenElements: CGFloat = 10.0
    private let imageViewWidth: CGFloat = 20.0

    private weak var spaceView: UIView?
    private weak var titleLabel: UILabel?
    private weak var imageView: UIImageView?
    
    var title: String? {
        willSet {
            self.titleLabel?.text = newValue
        }
    }
    
    // MARK: - LifeCycle
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonSetup()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.commonSetup()
    }
    
    // MARK: - Setup
    private func commonSetup() {
        let spaceView = UIView.init()
        self.spaceView = spaceView
        self.addSubview(spaceView)
        
        let titleLabel = UILabel.init()
        self.titleLabel = titleLabel
        self.titleLabel?.text = nil
        self.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
        self.titleLabel?.lineBreakMode = .byTruncatingTail
        self.titleLabel?.textAlignment = .center
        self.addSubview(titleLabel)
        
        let imageView = UIImageView.init()
        self.imageView = imageView
        self.imageView?.image = UIImage.init(named: "image_name")
        self.imageView?.contentMode = .scaleAspectFit
        self.addSubview(imageView)
        
        self.addConstraints()
    }
    
    // MARK: - Helper
    private func addConstraints() {
        guard let spaceView = self.spaceView,
              let titleLabel = self.titleLabel,
              let imageView = self.imageView else { return }
        
        let guide = self.safeAreaLayoutGuide
        
        self.spaceView?.translatesAutoresizingMaskIntoConstraints = false
        self.spaceView?.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
        self.spaceView?.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
        guide.bottomAnchor.constraint(equalTo: spaceView.bottomAnchor).isActive = true
        self.spaceView?.widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
        
        self.titleLabel?.translatesAutoresizingMaskIntoConstraints = false
        self.titleLabel?.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
        self.titleLabel?.leadingAnchor.constraint(equalTo: spaceView.trailingAnchor, constant: self.marginBetweenElements).isActive = true
        guide.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
        
        self.imageView?.translatesAutoresizingMaskIntoConstraints = false
        self.imageView?.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
        self.imageView?.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: self.marginBetweenElements).isActive = true
        guide.trailingAnchor.constraint(equalTo: imageView.trailingAnchor).isActive = true
        guide.bottomAnchor.constraint(equalTo: imageView.bottomAnchor).isActive = true
        self.imageView?.widthAnchor.constraint(equalToConstant: self.imageViewWidth).isActive = true
        self.imageView?.heightAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
    }
}

To use it:

 let centeredView = CenteredViewWithImage.init()
 self.centeredView = centeredView
 self.centeredView?.title = "text centered"
 let tapGesture: UITapGestureRecognizer = UITapGestureRecognizer.init(target: self, action: #selector(self.centeredViewHasBeenPressed))
 self.centeredView?.addGestureRecognizer(tapGesture)
 self.navigationItem.titleView = self.centeredView
    @objc
    private func centeredViewHasBeenPressed() {
        debugPrint("do something")
    }

How looks:

text centered on navigationbar

Fresneda
  • 103
  • 3
  • 6