In interface builder, holding Command + = will resize a button to fit its text. I was wondering if this was possible to do programmatically before the button was added to the view.
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button.titleLabel setFont:[UIFont fontWithName:@"Arial-BoldMT" size:12]];
[button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
// I need to know the width needed to accomodate NSStringVariable
[button setTitle:NSStringVariable forState:UIControlStateNormal];
// So that I can set the width property of the button before addSubview
[button setFrame:CGRectMake(10, 0, width, fixedHeight)];
[subNavigation addSubview:button];
In UIKit, there are additions to the NSString class to get from a given NSString object the size it'll take up when rendered in a certain font.
Docs was here. Now it's here under Deprecated.
In short, if you go:
CGSize stringsize = [myString sizeWithFont:[UIFont systemFontOfSize:14]];
//or whatever font you're using
[button setFrame:CGRectMake(10,0,stringsize.width, stringsize.height)];
...you'll have set the button's frame to the height and width of the string you're rendering.
You'll probably want to experiment with some buffer space around that CGSize, but you'll be starting in the right place.
The way to do this in code is:
[button sizeToFit];
If you are subclassing and want to add extra rules you can override:
- (CGSize)sizeThatFits:(CGSize)size;
-(CGSize)sizeThatFits:(CGSize)size
.
[button setTitle:@"Your text here" forState:UIControlStateNormal];
[button sizeToFit];
Also if you update text later, do not forget to call that again.
sizeToFit()
doesn't respect title edge insets.
If your button was made with Interface Builder, and you're changing the title in code, you can do this:
[self.button setTitle:@"Button Title" forState:UIControlStateNormal];
[self.button sizeToFit];
Swift:
The important thing here is when will you call the .sizeToFit()
, it should be after setting the text to display so it can read the size needed, if you later update text, then call it again.
var button = UIButton()
button.setTitle("Length of this text determines size", forState: UIControlState.Normal)
button.sizeToFit()
self.view.addSubview(button)
To accomplish this using autolayout, try setting a variable width constraint:
https://i.stack.imgur.com/iM1Vk.png
You may also need to adjust your Content Hugging Priority
and Content Compression Resistance Priority
to get the results you need.
UILabel is completely automatically self-sizing:
This UILabel is simply set to be centered on the screen (two constraints only, horizontal/vertical):
It changes widths totally automatically:
You do not need to set any width or height - it's totally automatic.
https://i.stack.imgur.com/Us3b2.png
https://i.stack.imgur.com/pVfMC.png
Notice the small yellow squares are simply attached ("spacing" of zero). They automatically move as the UILabel resizes.
Adding a ">=" constraint sets a minimum width for the UILabel:
https://i.stack.imgur.com/raRNQ.png
If you want to resize the text as opposed to the button, you can use ...
button.titleLabel.adjustsFontSizeToFitWidth = YES;
button.titleLabel.minimumScaleFactor = .5;
// The .5 value means that I will let it go down to half the original font size
// before the texts gets truncated
// note, if using anything pre ios6, you would use I think
button.titleLabel.minimumFontSize = 8;
sizeToFit doesn't work correctly. instead:
myButton.size = myButton.sizeThatFits(CGSize.zero)
you also can add contentInset
to the button:
myButton.contentEdgeInsets = UIEdgeInsetsMake(8, 8, 4, 8)
Swift 4.2
Thank god, this solved. After setting a text to the button, you can retrieve intrinsicContentSize which is the natural size from an UIView (the official document is here). For UIButton, you can use it like below.
button.intrinsicContentSize.width
For your information, I adjusted the width to make it look properly.
button.frame = CGRect(fx: xOffset, y: 0.0, width: button.intrinsicContentSize.width + 18, height: 40)
Simulator
UIButtons with intrinsicContentSize
Source: https://riptutorial.com/ios/example/16418/get-uibutton-s-size-strictly-based-on-its-text-and-font
Simply:
Create UIView as wrapper with auto layout to views around. Put UILabel inside that wrapper. Add constraints that will stick tyour label to edges of wrapper. Put UIButton inside your wrapper, then simple add the same constraints as you did for UILabel. Enjoy your autosized button along with text.
For some reason, func sizeToFit()
does not work for me. My set up is I am using a button inside a UITableViewCell
and I am using auto layout.
What worked for me is:
get the width constraint get the intrinsicContentSize width because according the this document auto layout is not aware of the intrinsicContentSize. Set the width constraint to the intrinsicContentSize width
https://i.stack.imgur.com/IoblM.png
https://i.stack.imgur.com/Jo445.png
https://i.stack.imgur.com/D8UKk.png
I have some additional needs related to this post that sizeToFit
does not solve. I needed to keep the button centered in it's original location, regardless of the text. I also never want the button to be larger than its original size.
So, I layout the button for its maximum size, save that size in the Controller and use the following method to size the button when the text changes:
+ (void) sizeButtonToText:(UIButton *)button availableSize:(CGSize)availableSize padding:(UIEdgeInsets)padding {
CGRect boundsForText = button.frame;
// Measures string
CGSize stringSize = [button.titleLabel.text sizeWithFont:button.titleLabel.font];
stringSize.width = MIN(stringSize.width + padding.left + padding.right, availableSize.width);
// Center's location of button
boundsForText.origin.x += (boundsForText.size.width - stringSize.width) / 2;
boundsForText.size.width = stringSize.width;
[button setFrame:boundsForText];
}
This button class with height autoresizing for text (for Xamarin but can be rewritten for other language)
[Register(nameof(ResizableButton))]
public class ResizableButton : UIButton
{
NSLayoutConstraint _heightConstraint;
public bool NeedsUpdateHeightConstraint { get; private set; } = true;
public ResizableButton(){}
public ResizableButton(UIButtonType type) : base(type){}
public ResizableButton(NSCoder coder) : base(coder){}
public ResizableButton(CGRect frame) : base(frame){}
protected ResizableButton(NSObjectFlag t) : base(t){}
protected internal ResizableButton(IntPtr handle) : base(handle){}
public override void LayoutSubviews()
{
base.LayoutSubviews();
UpdateHeightConstraint();
InvalidateIntrinsicContentSize();
}
public override void SetTitle(string title, UIControlState forState)
{
NeedsUpdateHeightConstraint = true;
base.SetTitle(title, forState);
}
private void UpdateHeightConstraint()
{
if (!NeedsUpdateHeightConstraint)
return;
NeedsUpdateHeightConstraint = false;
var labelSize = TitleLabel.SizeThatFits(new CGSize(Frame.Width - TitleEdgeInsets.Left - TitleEdgeInsets.Right, float.MaxValue));
var rect = new CGRect(Frame.X, Frame.Y, Frame.Width, labelSize.Height + TitleEdgeInsets.Top + TitleEdgeInsets.Bottom);
if (_heightConstraint != null)
RemoveConstraint(_heightConstraint);
_heightConstraint = NSLayoutConstraint.Create(this, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1, rect.Height);
AddConstraint(_heightConstraint);
}
public override CGSize IntrinsicContentSize
{
get
{
var labelSize = TitleLabel.SizeThatFits(new CGSize(Frame.Width - TitleEdgeInsets.Left - TitleEdgeInsets.Right, float.MaxValue));
return new CGSize(Frame.Width, labelSize.Height + TitleEdgeInsets.Top + TitleEdgeInsets.Bottom);
}
}
}
To be honest I think that it's really shame that there is no simple checkbox in storyboard to say that you want to resize buttons to accommodate the text. Well... whatever.
Here is the simplest solution using storyboard.
Place UIView and put constraints for it. Example: Place UILabel inside UIView. Set constraints to attach it to edges of UIView. Place your UIButton inside UIView. Set the same constraints to attach it to the edges of UIView. Set 0 for UILabel's number of lines. Set up the outlets.
@IBOutlet var button: UIButton!
@IBOutlet var textOnTheButton: UILabel!
Get some long, long, long title.
let someTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
On viewDidLoad set the title both for UILabel and UIButton.
override func viewDidLoad() {
super.viewDidLoad()
textOnTheButton.text = someTitle
button.setTitle(someTitle, for: .normal)
button.titleLabel?.numberOfLines = 0
}
Run it to make sure that button is resized and can be pressed.
https://i.stack.imgur.com/9qjfz.png
As sizeToFit
will ignore titleEdgeInsets
settings. In my case, the button title is fixed text, so I use autolayout to give each button a width constraint.
And I think if dynamic text length, you need to get the string size first and update the button constraint in viewDidLayoutSubview
.
lazy var importButton: UIButton = {
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("Import", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold)
btn.setTitleColor(.white, for: .normal)
btn.titleEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
btn.backgroundColor = .myBlue
btn.layer.cornerRadius = 8
btn.clipsToBounds = true
btn.addTarget(self, action: #selector(importButtonTapped), for: .touchUpInside)
return btn
}()
lazy var stitchButton: UIButton = {
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("Stitch", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 18, weight: .bold)
btn.setTitleColor(.white, for: .normal)
btn.titleEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
btn.backgroundColor = .myGreen
btn.layer.cornerRadius = 8
btn.clipsToBounds = true
btn.addTarget(self, action: #selector(stitchButtonTapped), for: .touchUpInside)
return btn
}()
func setViews() {
title = "Image Stitcher"
view.backgroundColor = .systemBackground
view.addSubview(importButton)
view.addSubview(stitchButton)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
NSLayoutConstraint(item: importButton, attribute: .centerX, relatedBy: .equal, toItem: g, attribute: .trailing, multiplier: 0.3, constant: 0),
importButton.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -50),
importButton.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.25),
importButton.heightAnchor.constraint(equalTo: importButton.widthAnchor, multiplier: 0.5),
NSLayoutConstraint(item: stitchButton, attribute: .centerX, relatedBy: .equal, toItem: g, attribute: .trailing, multiplier: 0.7, constant: 0),
stitchButton.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -50),
stitchButton.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.25),
stitchButton.heightAnchor.constraint(equalTo: stitchButton.widthAnchor, multiplier: 0.5),
])
}
https://i.stack.imgur.com/AJfza.jpg
In Xcode 4.5 and above, this can now be done by using 'Auto-layouting / Constraints'.
Major advantages are that:
You don't need to programmatically set frames at all! If done right, you don't need to bother about resetting frames for orientation changes. Also, device changes needn't bother you (read, no need to code separately for different screen sizes).
A few disadvantages:
Not backward compatible - works only for iOS 6 and above. Need to get familiarised (but will save time later on).
Coolest thing is we get to focus on declaring an intent such as:
I want these two buttons to be of the same width; or
I need this view to be vertically centered and extend to a max entent of 10 pts from the superview's edge; or even,
I want this button/label to resize according to the label it is displaying!
Here is a simple tutorial to get introduced to auto-layouting.
For a more details.
It takes some time at first, but it sure looks like it will be well worth the effort.
Success story sharing