UIButton that resizes to fit its titleLabel

47,901

Solution 1

I've gotten this to work, but you have to use a custom button, not a system type. Give the button both width and height constraints, and make an IBOutlet to the height constraint (heightCon in my code) so you can adjust it in code.

- (void)viewDidLoad {
    [super viewDidLoad];
    self.button.titleLabel.numberOfLines = 0;
    self.button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
    [self.button setTitle:@"A real real real real real real real real long long name." forState:UIControlStateNormal];
    [self.button addTarget:self action:@selector(doStuff:) forControlEvents:UIControlEventTouchUpInside];
    self.button.backgroundColor = [UIColor redColor];
    self.button.titleLabel.backgroundColor = [UIColor blueColor];
    [self.button layoutIfNeeded]; // need this to update the button's titleLabel's size
    self.heightCon.constant = self.button.titleLabel.frame.size.height;
}

After Edit:

I found that you can also do this more simply, and with a system button if you make a subclass, and use this code,

@implementation RDButton

-(CGSize)intrinsicContentSize {
    return CGSizeMake(self.frame.size.width, self.titleLabel.frame.size.height);
}

The overridden intrinsicContentSize method is called when you set the title. You shouldn't set a height constraint in this case.

Solution 2

Swift 4.x version of Kubba's answer:

Need to Update Line Break as Clip/WordWrap/ in Interface builder to corresponding buttons.

class ResizableButton: UIButton {
    override var intrinsicContentSize: CGSize {
       let labelSize = titleLabel?.sizeThatFits(CGSize(width: frame.width, height: .greatestFiniteMagnitude)) ?? .zero
       let desiredButtonSize = CGSize(width: labelSize.width + titleEdgeInsets.left + titleEdgeInsets.right, height: labelSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom)

       return desiredButtonSize
    }
}

Solution 3

Got same issue. It happens only if UIButton's titleLabel has more than one line. Is it a bug in UIKit?

My Swift solution:

class ResizableButton: UIButton {    
    override var intrinsicContentSize: CGSize {
        let labelSize = titleLabel?.sizeThatFits(CGSize(width: frame.size.width, height: CGFloat.greatestFiniteMagnitude)) ?? .zero
        let desiredButtonSize = CGSize(width: labelSize.width + titleEdgeInsets.left + titleEdgeInsets.right, height: labelSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom)

        return desiredButtonSize
    }
}

Solution 4

I struggled with this for a while and ended up making it work by subclassing UIButton and adding these two functions

class GoalsButton: UIButton {

    override var intrinsicContentSize: CGSize {
        return self.titleLabel!.intrinsicContentSize
    }

    // Whever the button is changed or needs to layout subviews,
    override func layoutSubviews() {
        super.layoutSubviews()
        titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
    }
}

Solution 5

I am away from my computer so I can't add the code right now, but I have found a workaround of this before.

What you can do is create a UILabel and add a UITapGestureRecognizer to the label. Do whatever you want for the button's action by handling the tap event. And also make sure that you enable user interactions on the UILabel.

This label will now behave as a auto resizing button.

Share:
47,901
Darren
Author by

Darren

Updated on July 09, 2022

Comments

  • Darren
    Darren almost 2 years

    I have a UIButton that I add to my view controller's view in a storyboard. I add centering constraints to position it and leading space constraints to limit its width. In code I add:

    self.button.titleLabel.numberOfLines = 0;
    self.button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
    [self.button setTitle:@"A real real real real real real real real long long name." forState:UIControlStateNormal];
    self.button.backgroundColor = [UIColor redColor];
    self.button.titleLabel.backgroundColor = [UIColor blueColor];
    

    The result is shown below:

    enter image description here

    I want the button to size to its content. How can I do this?

    I've tried

    [self.button sizeToFit];
    

    and I've tried setting the content hugging and compression resistance autolayout constraints priorities to required.

    I've also tried explicitly setting the contentEdgeInsets and titleEdgeInsets to UIEdgeInsetsZero and calling invalidateIntrinsicContentSize.

    I've also noticed that if I place newline characters in the title string, the button does seem to resize to fit its content.

    I'm running on Xcode 6 and iOS 8 on the iPhone 6 Simulator.

  • Darren
    Darren over 9 years
    This works exactly the way I want in terms of sizing, and it's what I tried first, but I want the text highlighting behaviour of a button when the user touches it.
  • Vlad
    Vlad about 8 years
    Hey Darren, you can set both UILabel.textColor and UILabel.highlightedTextColor properties. Then later you can update UILabel.highlighted property in order to change text color (for instance depends of UITapGestureRecognizer state). Gist
  • Neo
    Neo about 7 years
    You may want to consider adding some context ( An explanation ) around all of this code.
  • looseyi
    looseyi about 7 years
    well, as a common solution when cal the button title, we should know if the title is set by String, or NSAttributedString. so i add the value check, and cal extension. In the end, we should consider if the button also set the image to imageView, above all in consider, intrinsicContentSize should be right
  • xaphod
    xaphod almost 7 years
    Didn't work for me: titleLabel still exceeds the frame of the UIButton.
  • Ahmed Khedr
    Ahmed Khedr about 6 years
    @xaphod - forget to set the subclass in the attribute inspector?
  • Lal Krishna
    Lal Krishna about 5 years
    I added imageView's size and contentEdgeInsets too. to this code.
  • Daniel Kuta
    Daniel Kuta about 5 years
    It's work, but you should implement override func setTitle(_ title: String?, for state: UIControl.State) { super.setTitle(title, for: state) layoutIfNeeded() }
  • Alexander Khitev
    Alexander Khitev almost 5 years
    How can we animate this change?
  • Alexander Khitev
    Alexander Khitev almost 5 years
    How can we animate this change?
  • HaZe
    HaZe over 4 years
    Just a little completion: let with = labelSize.width + (image(for:[])?.size.width)! + titleEdgeInsets.left + titleEdgeInsets.right + imageEdgeInsets.left + imageEdgeInsets.right + contentEdgeInsets.left + contentEdgeInsets.right
  • Gerd Castan
    Gerd Castan over 4 years
    In my case, I have frame.width == 0.0. This leads to a wrong result (height of 1 line when the height of multiple lines would be correct). I had ti get a suitable width from somewhere else. So if this approach doesn't work, check the value of frame.width
  • Sergei Navka
    Sergei Navka almost 4 years
    @AlexanderKhitev you can call invalidateIntrinsicContentSize() in UIView.animation block