39

I have created a view controller that looks like this:

enter image description here

I want the two top buttons to always have 20 points between themselves and the left/right edges of the whole view. They should always have the same width too. I have created the constraints for all of this and it works exactly how I want it to. The problem is the vertical constraints. The buttons should always be 20 points beneath the top edge. They should have the same height. However, autolayout doesn't respect that the left label needs two lines to fit all its text, so the result looks like this:

enter image description here

I want it to look like in the first picture. I can't add constant height constraints to the buttons because when the app runs on iPad, only one line is needed and it would be wasteful to have extra space then.

In viewDidLoad I tried this:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.leftButton.titleLabel.preferredMaxLayoutWidth = (self.view.frame.size.width - 20.0 * 3) / 2.0;
    self.rightButton.titleLabel.preferredMaxLayoutWidth = (self.view.frame.size.width - 20.0 * 3) / 2.0;
}

But that did not change anyhting at all.

The question: How do I make autolayout respect that the left button needs two lines?

  • it might be because you have the "same height" constraint on the left button. That will make it the same height as the other buttons, changing the content size within the button – user2277872 May 24 '14 at 14:19
  • I tried to remove the "same height" constraint but it didn't work. –  May 24 '14 at 14:21
  • Have you tried adding >= constraints to the labels to establish a minimum inset? – jaggedcow May 24 '14 at 15:26
  • No, but I tried it now, but unfortunely it did not work either. Now on iPad, the buttons are "double high", which I don't want them to be. –  May 24 '14 at 15:32

17 Answers17

50

I had the same problem where I wanted my button to grow along with its title. I had to sublcass the UIButton and its intrinsicContentSize so that it returns the intrinsic size of the label.

- (CGSize)intrinsicContentSize
{
    return self.titleLabel.intrinsicContentSize;
}

Since the UILabel is multiline, its intrinsicContentSize is unknown and you have to set its preferredMaxLayoutWidth See objc.io article about that

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.frame.size.width;
    [super layoutSubviews];
}

The rest of the layout should work. If you set your both button having equal heights, the other one will grow to. The complete button looks like this

@implementation TAButton

- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        self.titleLabel.numberOfLines = 0;
        self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
    }
    return self;
}

- (CGSize)intrinsicContentSize
{
    return self.titleLabel.intrinsicContentSize;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.frame.size.width;
    [super layoutSubviews];
}

@end
Jan
  • 7,444
  • 9
  • 50
  • 74
  • Worked perfectly for me, thanks! I didn't need to set `numberOfLines` and call the second `[super layoutSubviews]` call though. – Dominic K Dec 11 '15 at 17:22
  • 2
    Thanks for this solution! Note that according to the article at objc.io, you don't need the first 'layoutSubviews' when subclassing. – Ariel Malka Feb 07 '18 at 11:34
  • 2
    for some reason, it didn't work for me. It froze my application I replaced the intrinsic content size with the following method and didn't override layout subviews `override var intrinsicContentSize: CGSize { let labelSize = titleLabel?.sizeThatFits(CGSize(width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude)) ?? CGSize.zero let reqiredButtonSize = CGSize(width: super.intrinsicContentSize.width, height: labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom) return reqiredButtonSize }` – iVentis Aug 30 '18 at 17:14
29

Swift 4.1.2 Version based on @Jan answer.

import UIKit

class MultiLineButton: UIButton {

    // MARK: - Init

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        self.commonInit()
    }

    private func commonInit() {
        self.titleLabel?.numberOfLines = 0
        self.titleLabel?.lineBreakMode = .byWordWrapping
    }

    // MARK: - Overrides

    override var intrinsicContentSize: CGSize {
        get {
             return titleLabel?.intrinsicContentSize ?? CGSize.zero
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        titleLabel?.preferredMaxLayoutWidth = titleLabel?.frame.size.width ?? 0
        super.layoutSubviews()
    }

}
Lena Schimmel
  • 7,203
  • 5
  • 43
  • 58
user3378170
  • 2,371
  • 22
  • 11
10

A simple solution working for me: make the multiline button to respect its title height in Swift 4.2 by adding a constraint for the button's height based on its title label's height:

let height = NSLayoutConstraint(item: multilineButton,
                                attribute: .height,
                                relatedBy: .equal,
                                toItem: multilineButton.titleLabel,
                                attribute: .height,
                                multiplier: 1,
                                constant: 0)
multilineButton.addConstraint(height)
Revanth Kausikan
  • 673
  • 1
  • 9
  • 21
Yevheniia Zelenska
  • 357
  • 1
  • 5
  • 13
9

This respects content edge insets and worked for me:

class MultilineButton: UIButton {

    func setup() {
        self.titleLabel?.numberOfLines = 0
        self.setContentHuggingPriority(UILayoutPriorityDefaultLow + 1, for: .vertical)
        self.setContentHuggingPriority(UILayoutPriorityDefaultLow + 1, for: .horizontal)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    override var intrinsicContentSize: CGSize {
        let size = self.titleLabel!.intrinsicContentSize
        return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
    }
}
Leszek Szary
  • 9,763
  • 4
  • 55
  • 62
6

add the missing constraints:

if let label = button.titleLabel {

    button.addConstraint(NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: button, attribute: .top, multiplier: 1.0, constant: 0.0))
    button.addConstraint(NSLayoutConstraint(item: label, attribute: .bottom, relatedBy: .equal, toItem: button, attribute: .bottom, multiplier: 1.0, constant: 0.0))
}
Hogdotmac
  • 282
  • 6
  • 7
3

Complete class in Swift 3 - based on @Jan, @Quantaliinuxite and @matt bezark:

@IBDesignable
class MultiLineButton:UIButton {

    //MARK: -
    //MARK: Setup
    func setup () {
        self.titleLabel?.numberOfLines = 0

        //The next two lines are essential in making sure autolayout sizes us correctly
        self.setContentHuggingPriority(UILayoutPriorityDefaultLow+1, for: .vertical)
        self.setContentHuggingPriority(UILayoutPriorityDefaultLow+1, for: .horizontal)
    }

    //MARK:-
    //MARK: Method overrides
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()
        titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
    }
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
3

There is a solution without subclassing on iOS11. Just need to set one additional constraint in code to match height of button and button.titleLabel.

ObjC:

// In init or overriden updateConstraints method
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.button
                                                              attribute:NSLayoutAttributeHeight
                                                              relatedBy:NSLayoutRelationEqual
                                                                 toItem:self.button.titleLabel
                                                              attribute:NSLayoutAttributeHeight
                                                             multiplier:1
                                                               constant:0];

[self addConstraint:constraint];

And in some cases (as said before):

- (void)layoutSubviews {
    [super layoutSubviews];

    self.button.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.button.titleLabel.frame);
}

Swift:

let constraint = NSLayoutConstraint(item: button,
                                    attribute: .height,
                                    relatedBy: .equal,
                                    toItem: button.titleLabel,
                                    attribute: .height,
                                    multiplier: 1,
                                    constant: 0)

self.addConstraint(constraint)

+

override func layoutSubviews() {
    super.layoutSubviews()

    button.titleLabel.preferredMaxLayoutWidth = button.titleLabel.frame.width
}
nemissm
  • 453
  • 6
  • 12
  • 1
    It's absolutely insane that I need to create constraints INSIDE the UIButton, butt his is the only solution that has worked for me. – Kenny Wyland Jul 10 '22 at 22:09
3

Lot of answers here, but the simple one by @Yevheniia Zelenska worked fine for me. Simplified Swift 5 version:

@IBOutlet private weak var button: UIButton! {
    didSet {
        guard let titleHeightAnchor = button.titleLabel?.heightAnchor else { return }
        button.heightAnchor.constraint(equalTo: titleHeightAnchor).isActive = true
    }
}
mmvdv
  • 497
  • 6
  • 11
2

None of the other answers had everything working for me. Here's my answer:

class MultilineButton: UIButton {
    func setup() {
        titleLabel?.textAlignment = .center
        titleLabel?.numberOfLines = 0
        titleLabel?.lineBreakMode = .byWordWrapping
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    override var intrinsicContentSize: CGSize {
        var titleContentSize = titleLabel?.intrinsicContentSize ?? CGSize.zero
        titleContentSize.height += contentEdgeInsets.top + contentEdgeInsets.bottom
        titleContentSize.width += contentEdgeInsets.left + contentEdgeInsets.right
        return titleContentSize
    }

    override func layoutSubviews() {
        titleLabel?.preferredMaxLayoutWidth = 300 // Or whatever your maximum is
        super.layoutSubviews()
    }
}

This won't cater for an image, however.

SeanR
  • 7,899
  • 6
  • 27
  • 38
  • This solution worked for me, specially adding contentEdgeInsets top, bottom, left, right to width and height of intrinsicContentSize of MultilineButton. – Abhishek Khedekar Mar 27 '20 at 08:28
1

Have you tried using this:

self.leftButton.titleLabel.textAlignment = NSTextAlignmentCenter;
self.leftButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping | NSLineBreakByTruncatingTail;
self.leftButton.titleLabel.numberOfLines = 0;
NullPointer
  • 195
  • 4
  • 18
1

UPDATED Swift/Swift 2.0 version again based on @Jan's answer

@IBDesignable
class MultiLineButton:UIButton {

  //MARK: -
  //MARK: Setup
  func setup () {
    self.titleLabel?.numberOfLines = 0

    //The next two lines are essential in making sure autolayout sizes us correctly
    self.setContentHuggingPriority(UILayoutPriorityDefaultLow+1, forAxis: .Vertical) 
    self.setContentHuggingPriority(UILayoutPriorityDefaultLow+1, forAxis: .Horizontal)
  }

  //MARK:-
  //MARK: Method overrides
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
  }

  override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
  }

  override func intrinsicContentSize() -> CGSize {
    return self.titleLabel!.intrinsicContentSize()
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
  }
}
Quantaliinuxite
  • 3,133
  • 4
  • 18
  • 32
1

tweaks for Swift 3.1

intrisicContentSize is a property instead of a function

override var intrinsicContentSize: CGSize {
    return self.titleLabel!.intrinsicContentSize
}
matt bezark
  • 236
  • 3
  • 5
1

Version, which also taking into account titleEdgeInsets and not overrides standard button behaviour unless titleLabel?.numberOfLines set to zero and button image set to nil.

open class Button: UIButton {

   override open var intrinsicContentSize: CGSize {
      if let titleLabel = titleLabel, titleLabel.numberOfLines == 0, image == nil {
         let size = titleLabel.intrinsicContentSize
         let result = CGSize(width: size.width + contentEdgeInsets.horizontal + titleEdgeInsets.horizontal,
                             height: size.height + contentEdgeInsets.vertical + titleEdgeInsets.vertical)
         return result
      } else {
         return super.intrinsicContentSize
      }
   }

   override open func layoutSubviews() {
      super.layoutSubviews()
      if let titleLabel = titleLabel, titleLabel.numberOfLines == 0, image == nil {
         let priority = UILayoutPriority.defaultLow + 1
         if titleLabel.horizontalContentHuggingPriority != priority {
            titleLabel.horizontalContentHuggingPriority = priority
         }
         if titleLabel.verticalContentHuggingPriority != priority {
            titleLabel.verticalContentHuggingPriority = priority
         }
         let rect = titleRect(forContentRect: contentRect(forBounds: bounds))
         titleLabel.preferredMaxLayoutWidth = rect.size.width
         super.layoutSubviews()
      }
   }
}
Vlad
  • 6,402
  • 1
  • 60
  • 74
1

I could not find a proper answer that took all these into account:

  1. Use AutoLayout only (meaning no override of layoutSubviews)
  2. Respect the button's contentEdgeInsets
  3. Minimalist (no playing with buttons's intrinsicContentSize)

So here's my take on it, which respects all three points from above.

final class MultilineButton: UIButton {

    /// Buttons don't have built-in layout support for multiline labels. 
    /// This constraint is here to provide proper button's height given titleLabel's height and contentEdgeInset.
    private var heightCorrectionConstraint: NSLayoutConstraint?
           
    override var contentEdgeInsets: UIEdgeInsets {
        didSet {
            heightCorrectionConstraint?.constant = -(contentEdgeInsets.top + contentEdgeInsets.bottom)
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayout()
    }
      
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupLayout()
    }
    
    private func setupLayout() {  
        titleLabel?.numberOfLines = 0
      
        heightCorrectionConstraint = titleLabel?.heightAnchor.constraint(equalTo: heightAnchor, constant: 0)
        heightCorrectionConstraint?.priority = .defaultHigh
        heightCorrectionConstraint?.isActive = true
    }
}

Note

I did not modify the button's intrinsicContentSize, there is no need to play with it. When the label is 2+ lines, the button's natural intrinsicContentSize's height is smaller than the desired height. The constraint that I added (heightCorrectionConstraint) corrects that automatically. Just make sure that the button's contentHuggingPriority in the vertical axis is smaller than the heightCorrectionConstraint's priority (which is the default).

Tumata
  • 1,507
  • 1
  • 11
  • 14
0

@Jan's answer doesn't work for me in (at least) iOS 8.1, 9.0 with Xcode 9.1. The problem: titleLabel's -intrinsicContentSize returns very big width and small height as there is no width limit at all (titleLabel.frame on call has zero size that leads to measurements problem). Moreover, it doesn't take into account possible insets and/or image.

So, here is my implementation that should fix all the stuff (only one method is really necessary):

@implementation PRButton

- (CGSize)intrinsicContentSize
{
    CGRect titleFrameMax = UIEdgeInsetsInsetRect(UIEdgeInsetsInsetRect(UIEdgeInsetsInsetRect(
        self.bounds, self.alignmentRectInsets), self.contentEdgeInsets), self.titleEdgeInsets
    );
    CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake(titleFrameMax.size.width, CGFLOAT_MAX)];

    CGSize superSize = [super intrinsicContentSize];
    return CGSizeMake(
        titleSize.width + (self.bounds.size.width - titleFrameMax.size.width),
        MAX(superSize.height, titleSize.height + (self.bounds.size.height - titleFrameMax.size.height))
    );
}

@end
kpower
  • 3,871
  • 4
  • 42
  • 62
0
//Swift 4 - Create Dynamic Button MultiLine Dynamic

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

         /// Add DemoButton 1
        let demoButton1 = buildButton("Demo 1")
        //demoButton1.addTarget(self, action: #selector(ViewController.onDemo1Tapped), for: .touchUpInside)
        view.addSubview(demoButton1)

        view.addConstraint(NSLayoutConstraint(item: demoButton1, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0))
        view.addConstraint(NSLayoutConstraint(item: demoButton1, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: -180))

    }

    func buildButton(_ title: String) -> UIButton {
        let button = UIButton(type: .system)
        button.backgroundColor = UIColor(red: 80/255, green: 70/255, blue: 66/255, alpha: 1.0)

        //--------------------------
        //to make the button multiline
        //button.titleLabel!.lineBreakMode = .byWordWrapping
        button.titleLabel?.textAlignment = .center
        button.titleLabel?.numberOfLines = 0
        //button.titleLabel?.adjustsFontSizeToFitWidth = true
        //button.sizeToFit()
        button.titleLabel?.preferredMaxLayoutWidth = self.view.bounds.width//200
        button.layer.borderWidth = 2
        let height = NSLayoutConstraint(item: button,
                                        attribute: .height,
                                        relatedBy: .equal,
                                        toItem: button.titleLabel,
                                        attribute: .height,
                                        multiplier: 1,
                                        constant: 0)
        button.addConstraint(height)
        //--------------------------

        button.setTitle(title, for: UIControlState())
        button.layer.cornerRadius = 4.0
        button.setTitleColor(UIColor(red: 233/255, green: 205/255, blue: 193/255, alpha: 1.0), for: UIControlState())
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }
}
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Moayadkom
  • 181
  • 6
0

Instead of calling layoutSubviews twice I'd calculate preferredMaxLayoutWidth manually

@objcMembers class MultilineButton: UIButton {

override var intrinsicContentSize: CGSize {
    // override to have the right height with autolayout
    get {
        var titleContentSize = titleLabel!.intrinsicContentSize
        titleContentSize.height += contentEdgeInsets.top + contentEdgeInsets.bottom
        return titleContentSize
    }
}

override func awakeFromNib() {
    super.awakeFromNib()
    titleLabel!.numberOfLines = 0
}

override func layoutSubviews() {
    let contentWidth = width - contentEdgeInsets.left - contentEdgeInsets.right
    let imageWidth = imageView?.width ?? 0 + imageEdgeInsets.left + imageEdgeInsets.right
    let titleMaxWidth = contentWidth - imageWidth - titleEdgeInsets.left - titleEdgeInsets.right

    titleLabel!.preferredMaxLayoutWidth = titleMaxWidth
    super.layoutSubviews()
}
}
Alexey
  • 440
  • 3
  • 12