简体   繁体   English

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

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

The iOS 9.0 comes with UIStackView which makes it easier to layout views according to their content size. iOS 9.0附带UIStackView,可以根据内容大小更轻松地布局视图。 For example, to place 3 buttons in a row in accordance with their content width you can simply embed them in stack view, set axis horizontal and distribution - fill proportionally. 例如,要根据内容宽度连续放置3个按钮,您只需将它们嵌入堆栈视图中,设置轴水平和分布 - 按比例填充。

Stack View解决方案

The question is how to achieve the same result in older iOS versions where stack view is not supported. 问题是如何在不支持堆栈视图的旧iOS版本中实现相同的结果。

One solution I came up with is rough and doesn't look good. 我提出的一个解决方案是粗糙并且看起来不太好。 Again, You place 3 buttons in a row and pin them to nearest neighbors using constraints. 再次,您将3个按钮连续放置并使用约束将它们固定到最近的邻居。 After doing that you obviously will see content priority ambiguity error because auto layout system has no idea which button needs to grow / shrink before others. 在这之后你显然会看到内容优先模糊错误,因为自动布局系统不知道哪个按钮需要在其他按钮之前增长/缩小。

内容优先模糊

Unfortunately, the titles are unknown before app's launch so you just might arbitrary pick a button. 不幸的是,在应用程序启动之前标题是未知的,因此您可能会随意选择一个按钮。 Let's say, I've decreased horizontal content hugging priority of middle button from standard 250 to 249. Now it'll grow before other two. 让我们说,我已经减少了从标准250到249的中间按钮优先级的水平内容。现在它将在其他两个之前增长。 Another problem is that left and right buttons strictly shrink to their content width without any nice looking paddings as in Stack View version. 另一个问题是,左侧和右侧按钮严格缩小到其内容宽度,而没有像Stack View版本那样漂亮的填充。

布局比较

It seems over complicated for a such simple thing. 对于这么简单的事情来说似乎过于复杂。 But the multiplier value of a constraint is read-only, so you'll have to go the hard way. 但是约束的乘数值是只读的,所以你必须采取艰难的方式。

I would do it like this if I had to: 如果我不得不这样做,我会这样做:

  1. In IB: Create a UIView with constraints to fill horizontally the superView (for example) 在IB中:创建一个带有约束的UIView来水平填充superView(例如)

  2. In IB: Add your 3 buttons, add contraints to align them horizontally. 在IB中:添加3个按钮,添加约束以水平对齐它们。

  3. In code: programmatically create 1 NSConstraint between each UIButton and the UIView with attribute NSLayoutAttributeWidth and multiplier of 0.33. 在代码中:以编程方式在每个UIButton和UIView之间创建1个NSConstraint,其属性为NSLayoutAttributeWidth ,乘数为0.33。

Here you will get 3 buttons of the same width using 1/3 of the UIView width. 在这里,您将获得3个相同宽度的按钮,使用1/3的UIView宽度。

  1. Observe the title of your buttons (use KVO or subclass UIButton). 观察按钮的title (使用KVO或子类UIButton)。 When the title changes, calculate the size of your button content with something like : 标题更改后,使用以下内容计算按钮内容的大小:

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

  2. Remove all programmatically created constraints. 删除所有以编程方式创建的约束

  3. Compare the calculated width (at step 4) of each button with the width of the UIView and determine a ratio for each button. 将每个按钮的计算宽度(步骤4)与UIView的宽度进行比较,并确定每个按钮的比例。

  4. Re-create the constraints of step 3 in the same way but replacing the 0.33 by the ratios calculated at step 6 and add them to the UI elements. 以相同的方式重新创建步骤3的约束,但用步骤6中计算的比率替换0.33,并将它们添加到UI元素。

Yes we can get the same results by using only constraints :) 是的,我们可以通过仅使用约束来获得相同的结果:)

source code 源代码

Imagine, I have three labels : 想象一下,我有三个标签:

  1. firstLabel with intrinsic content size equal to (62.5, 40) firstLabel,内在内容大小等于(62.5,40)
  2. secondLabel with intrinsic content size equal to (170.5, 40) secondLabel,内在内容大小等于(170.5,40)
  3. thirdLabel with intrinsic content size equal to (54, 40) thirdLabel,内在内容大小等于(54,40)

Strucuture Strucuture

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

Constraints 约束

for example the UIView has this constraints : view.leading = superview.leading, view.trailing = superview.trailing, and it is centered vertically 例如,UIView有这样的约束:view.leading = superview.leading,view.trailing = superview.trailing,它垂直居中

在此输入图像描述

UILabels constraints UILabels约束

在此输入图像描述

SecondLabel.width equal to: SecondLabel.width等于:

firstLabel.width * (secondLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth) firstLabel.width *(secondLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)

ThirdLabel.width equal to: ThirdLabel.width等于:

firstLabel.width * (thirdLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth) firstLabel.width *(thirdLabelIntrinsicSizeWidth / firstLabelIntrinsicSizeWidth)

I will back for more explanations 我会回来寻求更多解释

在此输入图像描述

You may want to consider a backport of UIStackView, there are several open source projects. 您可能想要考虑UIStackView的后端,有几个开源项目。 The benefit here is that eventually if you move to UIStackView you will have minimal code changes. 这里的好处是,如果您转移到UIStackView,您将获得最少的代码更改。 I've used TZStackView and it has worked admirably. 我使用过TZStackView ,它的工作非常出色。

Alternatively, a lighter weight solution would be to just replicate the logic for a proportional stack view layout. 或者,较轻的解决方案是复制逻辑以进行比例堆栈视图布局。

  1. Calculate total intrinsic content width of the views in your stack 计算堆栈中视图的总内在内容宽度
  2. Set the width of each view equal to the parent stack view multiplied by its proportion of the total intrinsic content width. 将每个视图的宽度设置为等于父堆栈视图乘以其总内在内容宽度的比例。

    I've attached a rough example of a horizontal proportional stack view below, you can run it in a Swift Playground. 我在下面附上了一个水平比例堆栈视图的粗略示例,您可以在Swift Playground中运行它。

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

Use autolayout to your advantage. 使用autolayout对您有利。 It can do all the heavy lifting for you. 它可以为你做所有繁重的工作。

Here is a UIViewController that lays out 3 UILabels , as you have in your screen shot, with no calculations. 这是一个UIViewController ,它可以在你的屏幕截图中显示3个UILabels ,无需计算。 There are 3 UIView subviews that are used to give the labels "padding" and set the background color. 有3个UIView子视图用于给标签“填充”并设置背景颜色。 Each of those UIViews has a UILabel subview that just shows the text and nothing else. 这些UIViews中的每一个都有一个UILabel子视图,只显示文本而不显示任何其他内容。

All of the layout is done with autolayout in viewDidLoad , which means no calculating ratios or frames and no KVO. 所有布局都是在viewDidLoad使用autolayout完成的,这意味着没有计算比率或帧,也没有KVO。 Changing things like padding and compression/hugging priorities is a breeze. 改变填充和压缩/拥抱优先级等事情是轻而易举的。 This also potentially avoids a dependency on an open source solution like TZStackView . 这也可能避免依赖于像TZStackView这样的开源解决方案。 This is just as easily setup in interface builder with absolutely no code needed. 这在界面构建器中很容易设置,绝对不需要代码。

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

}

Resulting view: 结果视图:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM