在 9.0 之前的 iOS 版本上模仿 UIStackView `按比例填充` 布局方法

Mimic UIStackView `fill proportionally` layout approach on iOS version prior to 9.0

iOS 9.0 附带了 UIStackView,这使得根据内容大小布局视图变得更加容易。例如,要根据内容宽度将 3 个按钮排成一行,您只需将它们嵌入到堆栈视图中,设置水平轴和分布 - 按比例填充。

问题是如何在不支持堆栈视图的旧 iOS 版本中实现相同的结果。

我想出的一个解决方案很粗糙而且看起来不太好。同样,您将 3 个按钮排成一行,并使用约束将它们固定到最近的邻居。这样做之后你显然会看到内容优先级歧义错误,因为自动布局系统不知道哪个按钮需要先于其他按钮增长/收缩。

不幸的是,这些标题在应用程序启动之前是未知的,因此您可能会随意选择一个按钮。比方说,我已经将中间按钮的水平内容拥抱优先级从标准的 250 降低到 249。现在它会先于其他两个增长。另一个问题是左右按钮严格缩小到它们的内容宽度,没有像堆栈视图版本中那样漂亮的填充。

这么简单的事情,似乎太复杂了。但是约束的乘数值是read-only,所以你必须走艰难的路。

如果必须的话,我会这样做:

  1. 在 IB 中:创建一个带有约束的 UIView 以水平填充 superView(例如)

  2. 在 IB 中:添加您的 3 个按钮,添加约束以将它们水平对齐。

  3. 在代码中:以编程方式在每个 UIButton 和具有属性 NSLayoutAttributeWidth 且乘数为 0.33 的 UIView 之间创建 1 个 NSConstraint。

在这里,您将使用 UIView 宽度的 1/3 得到 3 个相同宽度的按钮。

  1. 观察按钮的 title(使用 KVO 或子类 UIButton)。 当标题更改时,使用类似以下内容计算按钮内容的大小:

    CGSize stringsize = [myButton.title sizeWithAttributes: @{NSFontAttributeName: [UIFont systemFontOfSize:14.0f]}];

  2. 删除所有以编程方式创建的约束。

  3. 将每个按钮的计算宽度(在第 4 步)与 UIView 的宽度进行比较,并确定每个按钮的比率。

  4. Re-create 步骤 3 的约束以相同的方式但用步骤 6 计算的比率替换 0.33,并将它们添加到 UI 元素。

您可能需要考虑向后移植 UIStackView,有几个开源项目。这里的好处是,最终如果你移动到 UIStackView,你将有最少的代码更改。我用过 TZStackView,效果非常好。

或者,更轻量级的解决方案是只复制比例堆栈视图布局的逻辑。

  1. 计算堆栈中视图的总内在内容宽度
  2. 将每个视图的宽度设置为父堆栈视图乘以其在总固有内容宽度中所占的比例。

    我在下面附上了一个水平比例堆栈视图的粗略示例,您可以在 Swift 游乐场中 运行 它。

import UIKit
import XCPlayground

let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))

view.layer.borderWidth = 1
view.layer.borderColor = UIColor.grayColor().CGColor
view.backgroundColor = UIColor.whiteColor()

XCPlaygroundPage.currentPage.liveView = view

class ProportionalStackView: UIView {

  private var stackViewConstraints = [NSLayoutConstraint]()

  var arrangedSubviews: [UIView] {
    didSet {
      addArrangedSubviews()
      setNeedsUpdateConstraints()
    }
  }

  init(arrangedSubviews: [UIView]) {
    self.arrangedSubviews = arrangedSubviews
    super.init(frame: CGRectZero)
    addArrangedSubviews()
  }

  convenience init() {
    self.init(arrangedSubviews: [])
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func updateConstraints() {
    removeConstraints(stackViewConstraints)
    var newConstraints = [NSLayoutConstraint]()

    for (n, subview) in arrangedSubviews.enumerate() {

      newConstraints += buildVerticalConstraintsForSubview(subview)

      if n == 0 {
        newConstraints += buildLeadingConstraintsForLeadingSubview(subview)
      } else {
        newConstraints += buildConstraintsBetweenSubviews(arrangedSubviews[n-1], subviewB: subview)
      }

      if n == arrangedSubviews.count - 1 {
        newConstraints += buildTrailingConstraintsForTrailingSubview(subview)
      }

    }

    // for proportional widths, need to determine contribution of each subview to total content width
    let totalIntrinsicWidth = subviews.reduce(0) { [=10=] + .intrinsicContentSize().width }

    for subview in arrangedSubviews {
      let percentIntrinsicWidth = subview.intrinsicContentSize().width / totalIntrinsicWidth
      newConstraints.append(NSLayoutConstraint(item: subview, attribute: .Width, relatedBy: .Equal, toItem: self, attribute: .Width, multiplier: percentIntrinsicWidth, constant: 0))
    }

    addConstraints(newConstraints)
    stackViewConstraints = newConstraints

    super.updateConstraints()
  }

}

// Helper methods
extension ProportionalStackView {

  private func addArrangedSubviews() {
    for subview in arrangedSubviews {
      if subview.superview != self {
        subview.removeFromSuperview()
        addSubview(subview)
      }
    }
  }

  private func buildVerticalConstraintsForSubview(subview: UIView) -> [NSLayoutConstraint] {
    return NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
  }

  private func buildLeadingConstraintsForLeadingSubview(subview: UIView) -> [NSLayoutConstraint] {
    return NSLayoutConstraint.constraintsWithVisualFormat("|-0-[subview]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
  }

  private func buildConstraintsBetweenSubviews(subviewA: UIView, subviewB: UIView) -> [NSLayoutConstraint] {
    return NSLayoutConstraint.constraintsWithVisualFormat("[subviewA]-0-[subviewB]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subviewA": subviewA, "subviewB": subviewB])
  }

  private func buildTrailingConstraintsForTrailingSubview(subview: UIView) -> [NSLayoutConstraint] {
    return NSLayoutConstraint.constraintsWithVisualFormat("[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": subview])
  }

}

let labelA = UILabel()
labelA.text = "Foo"

let labelB = UILabel()
labelB.text = "FooBar"

let labelC = UILabel()
labelC.text = "FooBarBaz"

let stack = ProportionalStackView(arrangedSubviews: [labelA, labelB, labelC])

stack.translatesAutoresizingMaskIntoConstraints = false
labelA.translatesAutoresizingMaskIntoConstraints = false
labelB.translatesAutoresizingMaskIntoConstraints = false
labelC.translatesAutoresizingMaskIntoConstraints = false
labelA.backgroundColor = UIColor.orangeColor()
labelB.backgroundColor = UIColor.greenColor()
labelC.backgroundColor = UIColor.redColor()

view.addSubview(stack)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-0-[stack]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["stack": stack]))
view.addConstraint(NSLayoutConstraint(item: stack, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0))

使用自动版式对您有利。它可以为您完成所有繁重的工作。

这里有一个 UIViewController 布局 3 UILabels,正如您在屏幕截图中所做的那样,没有计算。有3个UIView个子view,用来给标签"padding"和设置背景色。每个 UIViews 都有一个 UILabel 子视图,只显示文本,没有其他内容。

viewDidLoad中所有的布局都是用自动布局完成的,这意味着没有计算比率或框架,也没有 KVO。更改填充和 compression/hugging 优先级等内容轻而易举。这也可能避免依赖像 TZStackView 这样的开源解决方案。这与在界面构建器中设置一样简单,完全不需要代码。

class StackViewController: UIViewController {

    private let leftView: UIView = {
        let leftView = UIView()
        leftView.translatesAutoresizingMaskIntoConstraints = false
        leftView.backgroundColor = .blueColor()
        return leftView
    }()

    private let leftLabel: UILabel = {
        let leftLabel = UILabel()
        leftLabel.translatesAutoresizingMaskIntoConstraints = false
        leftLabel.textColor = .whiteColor()
        leftLabel.text = "A medium title"
        leftLabel.textAlignment = .Center
        return leftLabel
    }()

    private let middleView: UIView = {
        let middleView = UIView()
        middleView.translatesAutoresizingMaskIntoConstraints = false
        middleView.backgroundColor = .redColor()
        return middleView
    }()

    private let middleLabel: UILabel = {
        let middleLabel = UILabel()
        middleLabel.translatesAutoresizingMaskIntoConstraints = false
        middleLabel.textColor = .whiteColor()
        middleLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
        middleLabel.textAlignment = .Center
        return middleLabel
    }()

    private let rightView: UIView = {
        let rightView = UIView()
        rightView.translatesAutoresizingMaskIntoConstraints = false
        rightView.backgroundColor = .greenColor()
        return rightView
    }()

    private let rightLabel: UILabel = {
        let rightLabel = UILabel()
        rightLabel.translatesAutoresizingMaskIntoConstraints = false
        rightLabel.textColor = .whiteColor()
        rightLabel.text = "OK"
        rightLabel.textAlignment = .Center
        return rightLabel
    }()


    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(leftView)
        view.addSubview(middleView)
        view.addSubview(rightView)

        leftView.addSubview(leftLabel)
        middleView.addSubview(middleLabel)
        rightView.addSubview(rightLabel)

        let views: [String : AnyObject] = [
            "topLayoutGuide" : topLayoutGuide,
            "leftView" : leftView,
            "leftLabel" : leftLabel,
            "middleView" : middleView,
            "middleLabel" : middleLabel,
            "rightView" : rightView,
            "rightLabel" : rightLabel
        ]

        // Horizontal padding for UILabels inside their respective UIViews
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[leftLabel]-(16)-|", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[middleLabel]-(16)-|", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(16)-[rightLabel]-(16)-|", options: [], metrics: nil, views: views))

        // Vertical padding for UILabels inside their respective UIViews
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[leftLabel]-(6)-|", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[middleLabel]-(6)-|", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(6)-[rightLabel]-(6)-|", options: [], metrics: nil, views: views))

        // Set the views' vertical position. The height can be determined from the label's intrinsic content size, so you only need to specify a y position to layout from. In this case, we specified the top of the screen.
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][leftView]", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][middleView]", options: [], metrics: nil, views: views))
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[topLayoutGuide][rightView]", options: [], metrics: nil, views: views))

        // Horizontal layout of views
        NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[leftView][middleView][rightView]|", options: [], metrics: nil, views: views))

        // Make sure the middle view is the view that expands to fill up the extra space
        middleLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
        middleView.setContentHuggingPriority(UILayoutPriorityDefaultLow, forAxis: .Horizontal)
    }

}

结果视图:

是的,我们可以通过仅使用约束获得相同的结果:)

source code

想象一下,我有三个标签:

  1. 内在内容大小等于 (62.5, 40)
  2. 的 firstLabel
  3. 内在内容大小等于 (170.5, 40) 的 secondLabel
  4. 固有内容大小等于 (54, 40) 的 thirdLabel

结构

-- ParentView --
    -- UIView -- (replace here the UIStackView)
       -- Label 1 -- 
       -- Label 2 -- 
       -- Label 3 --            

约束条件

例如 UIView 具有以下约束: view.leading = superview.leading, view.trailing = superview.trailing, 并且垂直居中

UILabels 约束

SecondLabel.width等于:

firstLabel.width * (secondLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)

ThirdLabel.width等于:

firstLabel.width * (thirdLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)

我会回来看更多的解释