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 it
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
}
}
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.
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
}
}
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];
/*
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
}
//...
}
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
.