UIButton that resizes to fit its titleLabel
Asked Answered
B

13

42

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.

Bantling answered 2/12, 2014 at 21:30 Comment(0)
S
23

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.

Simile answered 3/12, 2014 at 4:43 Comment(0)
C
43

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
    }
}
Crump answered 17/10, 2016 at 12:41 Comment(7)
Didn't work for me: titleLabel still exceeds the frame of the UIButton.Lattonia
@Lattonia - forget to set the subclass in the attribute inspector?Wallford
I added imageView's size and contentEdgeInsets too. to this code.Leaflet
It's work, but you should implement override func setTitle(_ title: String?, for state: UIControl.State) { super.setTitle(title, for: state) layoutIfNeeded() }Suffice
How can we animate this change?Jesseniajessey
Just a little completion: let with = labelSize.width + (image(for:[])?.size.width)! + titleEdgeInsets.left + titleEdgeInsets.right + imageEdgeInsets.left + imageEdgeInsets.right + contentEdgeInsets.left + contentEdgeInsets.rightWeirick
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.widthSheepish
S
23

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.

Simile answered 3/12, 2014 at 4:43 Comment(0)
C
23

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
    }
}
Colner answered 29/4, 2015 at 15:13 Comment(2)
How can we animate this change?Jesseniajessey
@AlexanderKhitev you can call invalidateIntrinsicContentSize() in UIView.animation blockTransept
B
7

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
    }
}
Beating answered 10/8, 2017 at 15:57 Comment(0)
V
2

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.

Vinylidene answered 3/12, 2014 at 3:39 Comment(2)
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.Bantling
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). GistScuffle
P
1

What I would suggest is to calculate the width of the text, and calculate the frame by yourself. It's not complicated anyway, get the width of the text first:

[NSString sizeWithFont:font];

Do a mod operation and you'll easily find out the number of lines for the text.

Note this method is for pre iOS7, for iOS 7 and after you might want to try

[NSString sizeWithAttributes:aDictionary];
Photosynthesis answered 3/12, 2014 at 4:22 Comment(0)
N
1

[self.button sizeToFit] should work if your Autolayout is turned Off. If you have to use autolayout then other suggestions (calculating line width) etc are more appropriate.

Norway answered 3/12, 2014 at 4:47 Comment(0)
C
1

I had the same problem with UIButton with multilined text, and it also had an image. I used sizeThatFits: to calculate the size but it calculated wrong height.

I did not make it UIButtonTypeCustom, instead of that I called sizeThatFits: on button's titleLabel with size with smaller width (due to image in button):

CGSize buttonSize = [button sizeThatFits:CGSizeMake(maxWidth, maxHeight)];
CGSize labelSize = [button.titleLabel sizeThatFits:CGSizeMake(maxWidth - offset, maxHeight)]; // offset for image
buttonSize.height = labelSize.height;
buttonFrame.size = buttonSize;

And then I used height from that size to set button's frame correctly, and it WORKED :)

Maybe they have some bug in internal sizing of UIButton.

Culosio answered 1/7, 2016 at 8:20 Comment(0)
A
1

Override the -(CGSize)intrinsicContentSize in Custom UIButton as given below.

Objective - C:

-(CGSize)intrinsicContentSize {
    CGSize titleLabelIntrinsicSize = [self.titleLabel intrinsicContentSize];
    return CGSizeMake(titleLabelIntrinsicSize.width + self.contentEdgeInsets.left + self.contentEdgeInsets.right, titleLabelIntrinsicSize.height + self.contentEdgeInsets.top + self.contentEdgeInsets.bottom);
}

Swfit :

override var intrinsicContentSize: CGSize {
        get {
            if let thisSize = self.titleLabel?.intrinsicContentSize {
                return CGSize(width: thisSize.width + self.contentEdgeInsets.left + self.contentEdgeInsets.right, height: thisSize.height + self.contentEdgeInsets.top + self.contentEdgeInsets.bottom)
            }
            return super.intrinsicContentSize
        }
    }
Attitude answered 31/7, 2017 at 7:39 Comment(0)
F
1

I had to invalidate the intrinsic content size when the views were laid out and then calculate the height of the button for the intrinsicContentSize property.

Here's the code in Swift3/Xcode 9

override func layoutSubviews() {
    self.invalidateIntrinsicContentSize()
    super.layoutSubviews()
}


override var intrinsicContentSize: CGSize {
    return CGSize(width: self.frame.size.width, height: titleLabel!.frame.size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
Flee answered 3/4, 2018 at 18:38 Comment(0)
S
1
/*
In SWIFT : Create an IBDesignable sub class of UIButton and override the intrinsicContentSize as shown below. It will resize appropriately for text and images. Then change your line break property to Word Wrap. Add a "fixedWidth" inspectable property that will adjust the height if you need buttons with fixed widths or when set to false will adjust the width and keep the height fixed.
*/

import UIKit

@IBDesignable
class UIButtonX: UIButton {
    
    //...

    @IBInspectable var fixedWidth : Bool = true
override var intrinsicContentSize: CGSize {
    let labelSize = titleLabel?.sizeThatFits(CGSize(width: fixedWidth ? frame.width : .greatestFiniteMagnitude, height: fixedWidth ? .greatestFiniteMagnitude : frame.height)) ?? .zero

    let wImage = image(for: [])?.size.width ?? 0
    let wTitleInset = titleEdgeInsets.left + titleEdgeInsets.right
    let wImageInset = imageEdgeInsets.left + imageEdgeInsets.right
    let wContentInset = contentEdgeInsets.left + contentEdgeInsets.right
    let width : CGFloat = labelSize.width + wImage + wTitleInset + wImageInset + wContentInset
    
    let biggerHeight = max(image(for: [])?.size.height ?? 0, labelSize.height)
    let hTitleInset = titleEdgeInsets.top + titleEdgeInsets.bottom
    let hImageInset = imageEdgeInsets.top + imageEdgeInsets.bottom
    let hContentInset = contentEdgeInsets.top + contentEdgeInsets.bottom
    let height : CGFloat = biggerHeight + hTitleInset + hImageInset + hContentInset
    
    let desiredButtonSize = CGSize(width: width, height: height)
    
    return desiredButtonSize
}
    
    //...

}
Shilohshim answered 25/6, 2020 at 20:19 Comment(0)
K
0
class SFResizableButton: UIButton {
  override var intrinsicContentSize: CGSize {
    get {
      var labelSize = CGSize.zero
        if let text = titleLabel?.text, let font = titleLabel?.font {
          labelSize.width = text.width(constrained: .greatestFiniteMagnitude, font: font)
        } else if let att = titleLabel?.attributedText {
          labelSize.width = att.width(constrained: .greatestFiniteMagnitude)
        }
      if let imageView = imageView {
        labelSize.width = labelSize.width + imageView.frame.width
      }
      let desiredButtonSize = CGSize(width: ceil(labelSize.width) + titleEdgeInsets.left + titleEdgeInsets.right + imageEdgeInsets.left + imageEdgeInsets.right, height: labelSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom + imageEdgeInsets.top + imageEdgeInsets.bottom)

        return desiredButtonSize
    }
  }
}
extesion String {
  func width(constrained height: CGFloat, font: UIFont) -> CGFloat {
    let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
      let boundingBox = (self as NSString).boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

      return boundingBox.width
  }
}

extension NSAttributedString {
  func width(constrained height: CGFloat) -> CGFloat {
    let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
      let boundingBox = boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)

      return boundingBox.width
  }
}
Katheykathi answered 10/2, 2017 at 15:17 Comment(2)
You may want to consider adding some context ( An explanation ) around all of this code.Mareah
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 rightKatheykathi
C
0

I made a subclass that works well inside UITableViewCell. It's needed if you want for example a border around the button like in the screenshot

UIButton with line breaks and a border

import Foundation
import UIKit

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

       return desiredButtonSize
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        invalidateIntrinsicContentSize()
    }
    
    func setup() {
        titleLabel?.lineBreakMode = .byWordWrapping
    }
    
    override func setTitle(_ title: String?, for state: UIControl.State) {
        super.setTitle(title, for: state)
        layoutIfNeeded()
    }
    
}
Cristalcristate answered 11/1, 2021 at 10:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.