【问题标题】:Multiline UIButton and autolayout多行 UIButton 和自动布局
【发布时间】:2014-07-13 19:15:16
【问题描述】:

我创建了一个如下所示的视图控制器:

我希望两个顶部按钮与整个视图的左/右边缘之间始终有 20 个点。它们也应该始终具有相同的宽度。我已经为所有这些创建了约束,它完全按照我想要的方式工作。问题是垂直约束。按钮应始终位于顶部边缘下方 20 点处。它们应该具有相同的高度。但是,自动布局不考虑左侧标签需要两行才能容纳其所有文本,因此结果如下所示:

我希望它看起来像第一张图片。我不能为按钮添加恒定的高度限制,因为当应用程序在 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;
}

但这并没有改变任何东西。

问题:如何让自动布局尊重左键需要两行?

【问题讨论】:

  • 这可能是因为您在左侧按钮上有“相同高度”约束。这将使其与其他按钮的高度相同,从而更改按钮内的内容大小
  • 我试图移除“相同高度”的限制,但没有成功。
  • 您是否尝试向标签添加 >= 约束以建立最小插入?
  • 不,但我现在尝试了,但不幸的是它也没有工作。现在在 iPad 上,按钮是“双高”,我不希望它们如此。

标签: ios uibutton autolayout


【解决方案1】:

我有同样的问题,我希望我的按钮随着它的标题一起增长。我必须将UIButton 及其intrinsicContentSize 子类化,以便它返回标签的固有大小。

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

由于UILabel是多行的,它的intrinsicContentSize是未知的,你必须设置它的preferredMaxLayoutWidthSee objc.io article about that

- (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] 调用。
  • 感谢您的解决方案!请注意,根据 objc.io 上的文章,子类化时不需要第一个 'layoutSubviews'。
  • 出于某种原因,它对我不起作用。它冻结了我的应用程序我用以下方法替换了固有内容大小并且没有覆盖布局子视图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 }
【解决方案2】:

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

}

【讨论】:

    【解决方案3】:

    这尊重内容边缘插图并为我工作:

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

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

        添加缺少的约束:

        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 子类更好。
        • 非常适合添加插图来修复自动布局问题,谢谢!
        【解决方案6】:

        这里有很多答案,但@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
            }
        }
        

        【讨论】:

          【解决方案7】:

          Swift 3 中的完整类 - 基于 @Jan、@Quantaliinuxite 和 @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
              }
          }
          

          【讨论】:

            【解决方案8】:

            你试过用这个吗:

            self.leftButton.titleLabel.textAlignment = NSTextAlignmentCenter;
            self.leftButton.titleLabel.lineBreakMode = NSLineBreakByWordWrapping | NSLineBreakByTruncatingTail;
            self.leftButton.titleLabel.numberOfLines = 0;
            

            【讨论】:

              【解决方案9】:

              根据@Jan 的回答再次更新 Swift/Swift 2.0 版本

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

              【讨论】:

                【解决方案10】:

                针对 Swift 3.1 的调整

                intrisicContentSize 是一个属性而不是一个函数

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

                【讨论】:

                  【解决方案11】:

                  iOS11 上有一个不用子类化的解决方案。只需要在代码中设置一个额外的约束来匹配buttonbutton.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];
                  

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

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

                  斯威夫特:

                  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
                  }
                  

                  【讨论】:

                    【解决方案12】:

                    没有其他答案对我有用。这是我的答案:

                    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 的宽度和高度。
                    【解决方案13】:

                    版本,它也考虑到titleEdgeInsets 并且不会覆盖标准按钮行为,除非titleLabel?.numberOfLines 设置为zero 并且按钮图像设置为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()
                          }
                       }
                    }
                    

                    【讨论】:

                      【解决方案14】:

                      @Jan 的答案在(至少)带有 Xcode 9.1 的 iOS 8.1、9.0 中对我不起作用。问题:titleLabel-intrinsicContentSize 返回非常大的宽度和非常小的高度,因为根本没有宽度限制(titleLabel.frame on call 的大小为零,导致测量问题)。此外,它没有考虑可能的插图和/或图像。

                      所以,这是我应该修复所有东西的实现(只有一种方法是真正需要的):

                      @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
                      

                      【讨论】:

                      • 这对我不起作用,而 Jan 的解决方案可以。
                      【解决方案15】:
                      //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
                          }
                      }
                      

                      【讨论】:

                        【解决方案16】:

                        我不会调用 layoutSubviews 两次,而是手动计算 preferredMaxLayoutWidth

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

                        【讨论】:

                          【解决方案17】:

                          我找不到将所有这些考虑在内的正确答案:

                          1. 仅使用 AutoLayout(意味着不覆盖 layoutSubviews
                          2. 尊重按钮的contentEdgeInsets
                          3. 极简主义(不玩按钮的intrinsicContentSize)

                          所以这是我的看法,它尊重上面的所有三点。

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

                          注意

                          我没有修改按钮的intrinsicContentSize,没有必要玩它。当标签超过 2 行时,按钮的自然 intrinsicContentSize 高度小于所需高度。我添加的约束 (heightCorrectionConstraint) 会自动更正该问题。只需确保按钮在垂直轴上的contentHuggingPriority 小于heightCorrectionConstraint 的优先级(这是默认值)。

                          【讨论】:

                            猜你喜欢
                            • 1970-01-01
                            • 1970-01-01
                            • 2015-09-01
                            • 1970-01-01
                            • 1970-01-01
                            • 1970-01-01
                            • 2017-07-18
                            • 2023-03-15
                            • 2016-06-04
                            相关资源
                            最近更新 更多