多行文本的UIButton和自动布局

39

我创建了一个视图控制器,看起来像这样:

enter image description here

我想让顶部的两个按钮始终与整个视图的左/右边缘保持20个点之间的距离。它们的宽度应该始终相同。我已经为所有这些创建了约束,并且它们的工作方式完全符合我的要求。问题在于垂直约束。按钮应该始终在顶部边缘下方20个点。它们应该具有相同的高度。然而,自动布局没有考虑到左侧标签需要两行来适应其所有文本,因此结果看起来像这样:

enter image description here

我希望它看起来像第一张图片。我不能向按钮添加常量高度约束,因为当应用程序在iPad上运行时,只需要一行,那将是浪费空间的。

viewDidLoad 中,我尝试了这个:

- (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;
}

但那并没有改变任何事情。

问题:我该如何让自动布局考虑到左侧按钮需要两行?


可能是因为您在左侧按钮上设置了“相同高度”约束。这将使它与其他按钮具有相同的高度,从而改变按钮内的内容大小。 - user2277872
我尝试移除“等高”约束,但是没有成功。 - user3124010
你尝试过在标签上添加">="约束条件来建立最小插入吗? - jaggedcow
不,但我现在尝试了一下,但不幸的是它仍然没有起作用。现在在iPad上,按钮是“双高”的,而我不希望它们是这样的。 - user3124010
17个回答

50

我也遇到了同样的问题,即我希望我的按钮能够根据其标题一起增长。我不得不对 UIButton进行子类化,并对其intrinsicContentSize进行子类化,以使其返回标签的内在大小。

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

由于 UILabel 具有多行文本的特性,其 intrinsicContentSize 是未知的,您需要设置其 preferredMaxLayoutWidth请参阅有关此的 objc.io 文章

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

布局的其他部分应该能正常工作。如果您将两个按钮的高度设置相等,则另一个按钮将会自动增长。完整的按钮如下所示:

@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

对我来说完美地工作了,谢谢!虽然我不需要设置 numberOfLines 并调用第二个 [super layoutSubviews] 调用。 - Dominic K
2
感谢您提供的解决方案!请注意,根据objc.io上的文章,在子类化时不需要第一个“layoutSubviews”。 - Ariel Malka
2
由于某些原因,它对我没有起作用。它冻结了我的应用程序。我用以下方法替换了内在内容大小,并没有覆盖布局子视图。`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

29

Swift 4.1.2 版本,基于 @Jan 的回答。

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()
    }

}

10

以下是适合我的简单解决方案:通过为按钮的高度添加一个基于标题标签高度的约束,使多行按钮在Swift 4.2中尊重其标题高度:

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

9

这个方法考虑了内容边缘插入,并且对我有用:

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
    }
}

6

添加缺失的约束条件:

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))
}

这对我非常有效,似乎比需要一个UIButton子类更好的解决方案。 - marcshilling
添加插入以解决自动布局问题后,效果非常好,谢谢! - thibaut noah

3

基于@Jan、@Quantaliinuxite和@matt bezark的Swift 3完整课程:

@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
    }
}

3
在iOS11中,有一种不需要子类化的解决方案。只需在代码中设置一个额外的约束来匹配和按钮标题的高度即可。 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];

在某些情况下(如前所述):

- (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
}

1
简直不可思议,我需要在UIButton内创建约束,但这是唯一有效的解决方案。 - Kenny Wyland

3

这里有很多答案,但是 @Yevheniia Zelenska 的简单版本对我很有效。Swift 5 简化版代码如下:

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

2

其他答案对我来说都不完全适用。这是我的答案:

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()
    }
}

这不包括图片,然而。

这个解决方案对我很有效,特别是将contentEdgeInsets的top、bottom、left和right添加到MultilineButton的intrinsicContentSize的宽度和高度中。 - Abhishek Khedekar

1

更新的Swift/Swift 2.0版本,再次基于@Jan的答案

@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
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接