在9.0

时间:2015-10-17 11:44:21

标签: ios ios-autolayout

iOS 9.0附带UIStackView,可以根据内容大小更轻松地布局视图。例如,要根据内容宽度连续放置3个按钮,只需将它们嵌入堆栈视图中,设置轴水平和分布 - 按比例填充。

Stack View Solution

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

我提出的一个解决方案是粗糙的并且看起来不太好。再次,您将3个按钮连续放置并使用约束将它们固定到最近的邻居。在这之后你显然会看到内容优先模糊错误,因为自动布局系统不知道哪个按钮需要在其他按钮之前增长/缩小。

Content Priority Ambiguity

不幸的是,在应用程序发布之前标题是未知的,所以你可能随意选择一个按钮。让我们说,我已经将中间按钮的水平内容优先级从标准250降低到249.现在它会在其他两个之前增长。另一个问题是左右按钮严格缩小到其内容宽度,而没有像Stack View版本那样漂亮的填充。

Layout Comparsion

4 个答案:

答案 0 :(得分:5)

对于这么简单的事情来说似乎过于复杂。但约束的乘数值是只读的,所以你必须采取艰难的方式。

如果我不得不这样做,我会这样做:

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

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

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

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

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

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

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

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

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

答案 1 :(得分:4)

是的,我们只能使用约束来获得相同的结果:)

source code

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

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

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

    <强>约束

    例如UIView有这样的约束: view.leading = superview.leading,view.trailing = superview.trailing,它垂直居中

    enter image description here

    UILabels约束

    enter image description here

    SecondLabel.width等于:

    firstLabel.width *(secondLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)

    ThirdLabel.width等于:

    firstLabel.width *(thirdLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)

    我会回来寻求更多解释

    enter image description here

答案 2 :(得分:2)

您可能想要考虑UIStackView的后端,有几个开源项目。这里的好处是,如果您转移到UIStackView,您将获得最少的代码更改。我已经使用了TZStackView并且它的效果令人钦佩。

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

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

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

  3. 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) { $0 + $1.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))
    

答案 3 :(得分:2)

使用autolayout对您有利。它可以为你做所有繁重的工作。

这是一个UIViewController,可以在屏幕截图中显示3 UILabels,无需计算。有3个UIView子视图用于为标签“填充”并设置背景颜色。这些UIViews中的每一个都有UILabel子视图,只显示文本而不显示任何其他内容。

所有布局都在viewDidLoad中使用自动布局完成,这意味着没有计算比率或帧,也没有KVO。改变填充和压缩/拥抱优先级等事情是轻而易举的。这也可能避免依赖于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)
    }

}

结果视图: