10

I have a UIButton and it can change the title at the runtime. Therefore, I want to increase the UIButton height depend on the title text for display full text by using AutoLayout.

I can increase the UILabel height by set the height constraint to "Greater than or Equal" but it not work with UIButton.
I have used [myButton sizeToFit] but it only increase the UIButon width (not increase height).

My current UIButton properties now is
- constraint height: 30 - leading : 15 - trailing: 15 - top: 5 - fontsize: 12

UPDATE
I created an IBOutlet for constraint height of UIButton for changing the height as @NSNood said.
Then I need to use \n in title text to split line.
But I don't know where should I put the \n?

Here is the Button that I want in portrait

enter image description here

Here is the Button that I want in landscape enter image description here

How can I determine the place to put \n?

Please guide me how to achieve it with AutoLayout. Any help would be appreciated.

Linh
  • 57,942
  • 23
  • 262
  • 279
  • 1
    Creare IBOutlet of height constraint of button and change it's constant when you want to. E.g. If you want to set set height equal to 30, you'd do this: `btnHeightConstraint.constant = 30`. – NSNoob Jan 04 '16 at 10:06
  • 1
    when I only increase the constrainst height of button, the text will not split to 2 lines, it still in 1 line – Linh Jan 04 '16 at 10:07
  • If title is able to fit in given width of the button, it will not split into two lines. If you want to set multiple lined title anyways, you could set an attributed string as title with `\n`. – NSNoob Jan 04 '16 at 10:10
  • but if I run the application on bigger device, or I rotate the screen, the button text will display bad – Linh Jan 04 '16 at 10:13
  • If you have applied proper constraints, then it wouldn't look bad on bigger devices. Idk about rotation scenario. – NSNoob Jan 04 '16 at 10:17
  • I have update my `UIButton` constraint, please see it – Linh Jan 04 '16 at 10:18
  • Your button is missing a vertical position constraint. – NSNoob Jan 04 '16 at 10:20
  • If you need your button to increase in size, you need to set all 4 constraints. I think you are missing the bottom constraint here. Also `sizeToFit` is not meant for Auto Layout. For AutoLayout, you need to use `systemLayoutSizeFittingSize:`. – Ayan Sengupta Jan 04 '16 at 10:21
  • See this answer: http://stackoverflow.com/questions/604632/how-do-you-add-multi-line-text-to-a-uibutton – Nailer Jan 04 '16 at 12:05

4 Answers4

11

Sorry that I didn't follow the post, lately and thus am coming up with a real late solution. Still I'm writing the answer as a reference, if someone might find it useful in future.

First of all let's show the storyboard configuration for the button. Those are depicted in the following pictures:

Constraint configuration

The picture shows that I have added only left, top and right constraints for the button and nothing else. This allows the button to have some intrinsicContentSize for it's height but it's width is still determined by it's left and right constraints.

The next phase is to write some ViewController class that shall contain the button. In my VC, I have created an outlet for the button by name button:

@property(nonatomic,weak) IBOutlet UIButton* button;

and has attached it to the storyboard button. Now I have overridden two methods, namely, viewDidLoad and viewWillLayoutSubviews like below:

-(void)viewDidLoad {
    [super viewDidLoad];

    self.button.titleLabel.numberOfLines = 0;
    self.button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
}
-(void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    [self.button setTitle:@"Chapter One\n "
     "A Stop on the Salt Route\n "
     "1000 B.C.\n "
     "As they rounded a bend in the path that ran beside the river, Lara recognized the silhouette of a fig tree atop a nearby hill. The weather was hot and the days were long. The fig tree was in full leaf, but not yet bearing fruit." forState:UIControlStateNormal];
}
  1. The viewDidLoad method ensures the titleLabel (the label that holds button text) is multiline and if some large text comes to it, it wraps the text by wrapping words.
  2. The viewWillLayoutSubviews method ensures button layouting process occurs whenever bounds of the main view change, e.g. due to the change of interface orientation.

The final and the most effective part is to manually handle the layout process for the button. For this purpose, we need to subclass UIButton. I have written a subclass named MyButton that inherits from UIButton and you might use whatever name you like. Set this as the custom class for the button in Identity Inspector.

The subclass overrides two methods, namely, intrinsicContentSize and layoutSubviews. The class body looks something like the following:

#import "MyButton.h"

@implementation MyButton

-(CGSize)intrinsicContentSize {
    return [self.titleLabel sizeThatFits:CGSizeMake(self.titleLabel.preferredMaxLayoutWidth, CGFLOAT_MAX)];;
}
-(void)layoutSubviews {
    self.titleLabel.preferredMaxLayoutWidth = self.frame.size.width;
    [super layoutSubviews];
}
@end

The UIButon subclass takes the ownership of the layout process by overriding layoutSubviews method. The basic idea here is to determine the button width, once it has been layout. Then setting the width as preferredMaxLayoutWidth (the maximum width for layouting engine, that a multiline label should occupy) of it's child titleLabel (the label that holds button text). Finally, returning an intrinsicContentSize for the button based on it's titleLabel's size, so that the button fully wraps it's titleLabel.

  1. The overridden layoutSubviews is called when the button is already layed out and it's frame size is determined. At it's first step, button's rendered width is set as preferredMaxLayoutWidth of the button's titleLabel.
  2. The second step re-invokes the layouting engine by calling [super layoutSubviews], so that the buttons intrinsicContentSize is re-determined based on it's titleLabel's preferredMaxLayoutWidth, which is set to buttons rendered width, by now.
  3. In the overridden intrinsicContentSize method we return the minimum fitting size for the button that fully wraps it's titleLabel with preferredMaxLayoutWidth set. We use sizeThatFits fits method on the button's titleLabel and that simply works as titleLabel doesn't follow any constraint based layout.

The outcome should be something similar to that you might have required.

Portrait Landscape

Feel free to let me know about any other clarification/concern.

Thanks.

Ayan Sengupta
  • 5,238
  • 2
  • 28
  • 40
  • thank you so much for your solution. however, I have a question, with your code, the button will have 4 lines in portrait and when we rotate it still have 4 lines in landscape right? But in my image post, my button have 4 lines in portrait and 3 lines in landscape. Correct me if I'm wrong. Thank you – Linh Jan 05 '16 at 01:13
  • Have you tried that in your device? I haven't tried my code in real device but I have tried on simulator running iOS 9.2. You could see the output for each orientation in my updated answer. The button should show only the required number of lines based on available space. Additionally I have added some border to button layer to show it's bounding rect. Please let me know. Thanks! – Ayan Sengupta Jan 05 '16 at 12:47
  • `UIButton`'s subclass is good solution I think. In case you don't want to subclass `UIButton` you still can create wrapper view. – Timur Bernikovich Jun 22 '17 at 20:45
  • There is a small bug in this answer. The `preferredMaxLayoutWidth` needs to be taking into account the `contentEdgeInsets` and the `titleEdgeInsets` to calculate the correct width. – Claus Jørgensen Jul 10 '20 at 08:29
  • @ClausJørgensen you are kinda correct. We can also consider those parameters if we want to, along with `imageEdgeInsets`. But I simply didn't want to make my answer complicated and give out only the basic idea. Considering those other parameters left as an exercise for anyone interested ;) but that is simply not a bug by definition. – Ayan Sengupta Jul 19 '20 at 09:10
4

Ayan Sengupta solution in Swift, with support for contentEdgeInsets (thanks Claus Jørgensen):

(You may also further customize the code to take titleEdgeInsets into account if needed)

  1. Subclass your UIButton to take the ownership of the layout process:

     /// https://stackoverflow.com/a/50575588/1033581
     class AutoLayoutButton: UIButton {
         override var intrinsicContentSize: CGSize {
             var size = titleLabel!.sizeThatFits(CGSize(width: titleLabel!.preferredMaxLayoutWidth - contentEdgeInsets.left - contentEdgeInsets.right, height: .greatestFiniteMagnitude))
             size.height += contentEdgeInsets.left + contentEdgeInsets.right
             return size
         }
         override func layoutSubviews() {
             titleLabel?.preferredMaxLayoutWidth = frame.size.width
             super.layoutSubviews()
         }
     }
    
  2. Use this class in your storyboard, and set constraints for Leading, Trailing, Top, Bottom. But don't set any Height constraint.

An alternative without subclassing is to add a wrapper view as suggested by Bartłomiej Semańczyk answer and Timur Bernikowich comment.

Cœur
  • 37,241
  • 25
  • 195
  • 267
  • FYI, there's a bug in both answers. The `preferredMaxLayoutWidth` needs to be taking into account the `contentEdgeInsets` and the `titleEdgeInsets` to calculate the correct width. – Claus Jørgensen Jul 10 '20 at 08:29
  • 1
    @ClausJørgensen thank you, I've added support to `contentEdgeInsets`. Regarding `titleEdgeInsets`, its documentation reads "_The button does not use this property to determine intrinsicContentSize_", so I'll ignore it for my answer, but feel free to customize the code for your needs. – Cœur Jul 11 '20 at 18:14
1

The point is that if you set sizeToFit property, then the text will always be in one line and the width of the button will increase unless you put a next-line sign \n to explicitly say that you want it to be several lines.

You put '\n' in the end of the first line like "line \n line" which represents

line
line

If you want to have two different string values (with \n positioned differently) for Portrait and Landscape you can check the orientation condition using UIDeviceOrientation (UIDevice.currentDevice.orientation) described here and set a string value depending on the orientation of the device

tmac_balla
  • 648
  • 3
  • 16
0

There is a way I always used:

  1. Add another reference UILabel which lineNumber=0 and the same width with the target button.
  2. Do not set height constraint for the ref-UILable, and should set a height constraint for the button to adjust its height
  3. Set the same text to the ref UILabel with the button.titleLable, sizeTofit it and get its frame.size.height
  4. Use the height value to the height constraint of the target button. (Of course, the button.titleLabel linenumber should be set to 0 or more lines)

Done. :)

PS1. This way can be used for the button and ref-label in a scrollview.

PS2. In some case, we can not get the correct height of the ref-label because it cannot gain a correct frame.width in scrollview, especially when we use the trailling constraint. We could consider to define a fixed width to the ref-label before sizeTofit and obtain the correct height for target button use.

Shrdi
  • 359
  • 4
  • 13