重学 AutoLayout (1) — Intrinsic Content Size

最近闲来无事, 把AutoLayout的内容收拾收拾.

要害的内容 – 划要点

  1. 控件的IntrinsicContentSize巨细
  2. CHCR是处理抵触的钥匙
  3. Constrain束缚都有Priority
  4. systemLayoutSizeFitting是束缚和Frame的枢纽

1. Anchor 和 Constrain

iOS 的AutoLayout 是基于cassowary算法, 在开发人员设置好各个view的束缚信息今后, NSISEnginer管帐算出每个view的frame. 最终布局运用的view的frame进行布局.

留意 AutoLayout 与 AutoSizeMask 不能共存, 假如需求对某个View运用AutoLayout, 需求优先运用view.translatesAutoresizingMaskIntoConstraints = false

在咱们运用Anchor去进行界面的束缚布局时, 常常会碰到成果现象和预期不符, 乃至束缚抵触的问题.

例如, 咱们的一个 UILabelUITextFiled在一条线上, 咱们预期UITextFiled拉伸, 可是成果是UILabel被拉伸了, Demo代码如下:

func setupSubViews() {
    let label = makeLabel(withText: "Name")
    let textFiled = makeTextFiled(withPlaceHolderText: "Enter name here")
    view.addSubview(label)
    view.addSubview(textFiled)
    NSLayoutConstraint.activate([
        // label anchor
        label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
        label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8),
        label.rightAnchor.constraint(equalTo: textFiled.leftAnchor, constant: -8),
        // textFiled anchor
        textFiled.topAnchor.constraint(equalTo: label.topAnchor),
        textFiled.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8),
    ])
}
func makeLabel(withText text: String) -> UILabel {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = text
        label.backgroundColor = .yellow
        return label
    }
func makeTextFiled(withPlaceHolderText text: String) -> UITextField {
        let label = UITextField()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = text
        label.backgroundColor = .lightGray
        return label
    }

重学AutoLayout (1) -- Intrinsic Content Size

why??? 本文便是来处理这里的疑惑的!!!

2. IntrinsicContentSize

在运用AutoLayout时, 咱们对 View的IntrinsicContentSize 的了解十分要害.

咱们需求时间牢记, 每个View都有天然巨细, 也就这个View假如没有外部束缚情况下, 它的CGSize是多少!!!

关于系统的UI控件来说他们都有自己的IntrinsicContentSize巨细. 也便是说大部分的UIKit控件都能size themselves, 下面是一些常见的控件的IntrinsicContentSize 信息

  1. UISwitch - (49, 31)
  2. UIActivityIdicator - Small: (20, 20) Big:(37, 37)
  3. UIButton - 内部的Label的size + padding
  4. UILabel - The size that fits its text!!! (这是在label的宽度没有束缚的情况下!!! 后边有特别)
  5. UIImageView: - The size of the image(假如没有图画, 那么是(0, 0))
  6. UIView - has no intrinsic conent size (或者说是 (-1, -1))

留意, 从上面的概念来说 1~5 都是具有IntrinsicContentSize的, 也便是它们能够根据自己的内容size themselves, 而原生的UIView不可的.

2.1 UIView

UIView 在默许情况下IntrinsicContentSize(-1, -1), 这表明UIView默许情况下是没有天然巨细的, 或者天然巨细是(UIView.noIntrinsicMetric, UIView.noIntrinsicMetric)

除了直接设置widthAnchorheightAnchor强制束缚UIView的巨细, 另外一种办法便是自定义承继, 重写该办法:

class BigView: UIView {
    // 具有天然巨细的 UIView
    override var intrinsicContentSize: CGSize{
        return CGSize(width: 200, height: 100)
    }
}
class MyView: UIView {
    // 默许的天然巨细是 (-1, -1), 是无法参与 AutoLayout 核算的!!!
    override var intrinsicContentSize: CGSize{
        let size = super.intrinsicContentSize
        return size
    }
}

假如后续要运用MyView, 就能像UIlabel那样能具有自己的固有尺寸!

2.2 UILabel

class MyLabel: UILabel {
  override var intrinsicContentSize: CGSize {
    let size1 = super.intrinsicContentSize
    let size2 = sizeThatFits(UIView.layoutFittingCompressedSize)
    print("UILabel intrinsicContentSize: \(size1)")
    print("UILabel sizeThatFits: \(size2)")
    return size1
  }
}
  1. label.text = "", 此时没有内容, 打印成果如下:
UILabel intrinsicContentSize: (0.0, 0.0)
UILabel sizeThatFits: (0.0, 0.0)
  1. label.text = "123123123131313232332"
UILabel intrinsicContentSize: (196.0, 20.333333333333332)
UILabel sizeThatFits: (196.0, 20.333333333333332)
  1. label.text = "123123123131313232332", 并且label.preferredMaxLayoutWidth = 100, label.numberOfLines = 0:
UILabel intrinsicContentSize: (96.0, 61.0)
UILabel sizeThatFits: (196.0, 20.333333333333332)

在以上的基础上:

let myLabel = MyLabel()
myLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myLabel)
myLabel.text = "123123123131313232332"
myLabel.preferredMaxLayoutWidth = 100
myLabel.numberOfLines = 0
let layoutSize = myLabel.systemLayoutSizeFitting(CGSize(width: 100, height: UIView.noIntrinsicMetric))
print("layoutSize: \(layoutSize)")

能看到输出成果:

layoutSize: (96.0, 61.0)
UILabel intrinsicContentSize: (96.0, 61.0)
UILabel sizeThatFits: (196.0, 20.333333333333332)

留意:

  1. preferredMaxLayoutWidth 和 numberOfLines 需求一同运用才会收效!
  2. preferredMaxLayoutWidth 能够简略了解成, 在text文本长度很长时, label的 intrinsicContent 的 intrinsic width 的预算值
  3. sizeThatFits 办法同 systemLayoutSizeFitting

2.3 ImageView

ImageView 同UIlabel相似, 在没有设置image特点时, 天然巨细为(0, 0), 当设置了image今后, 天然巨细为图画的size

3. CHCR与束缚优先级

假设一个自定义Label的Intrinsic content size(width: 50 ,height: 20).

(50, 20) 表明 Label的天然巨细!!! 可是这仅仅是Label自己的希望巨细, 是 Optional的!!!, 假如有额外的束缚使得无法满意的这个希望怎么办??? 这里就需求了解CHCR: CH 表明是Content Hugging; CR 表明Content Compression Resistance.

  1. Content Hugging: label.width <= 50, priority = defaultLow(250)
  2. Content Compression Resistance: label.width >= 50, priority = defaultHight(750)

简略来说, Intrinsic Size 中的 width 便是给NSISEnginer输入了两个width关联的带有指定优先级的束缚条件!!! (height 相似)

而咱们自己创建的Anchor Constrain默许优先级都是priority = required(1000)

因而, 只要咱们常规办法手动设置一个水平方向束缚给label, 那么就会打破它的CHCH的束缚条件, 而常见的设置label的 width 束缚有两种:

  1. label.leftAnchor + label.rightAnchor
  2. label.widthAnchor

再来看看文章开头的问题!!!

假如咱们需求Label不要被拉伸, 那么需求修正label.CH 的 priority > textFiled.CH 的 priority, 因而咱们只需求添加一句话即可:

// 办法1: 水平方向上: label.chp 251 > textField.chp 250, 这样水平束缚上, TextFiled会被拉伸!!!
label.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .horizontal)
// 办法2: 水平方向上: label.chp 250 > textFiled.chp 249 与上面相似
textFiled.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .horizontal)

因而AutoLayout小结一下:

  1. 要害概念: Intrinsic SizeOptional, 仅仅控件的主张, 能够被打破!!!
  2. 问题转化: 束缚 anbiguous 或者 束缚抵触, 要转化成束缚抵触方向各个View的CHCR的Priority的问题!!!
  3. 处理办法: 修正CHCR Priority. 具体来说: CH表明 <=, 并priority = 250; CR表明>=, priority = 750; 假如自己不想被拉伸, 那么添加CHP, 假如自己不想被紧缩, 添加CRP
  4. 与Frame联系: systemLayoutSizeFittingtranslatesAutoresizingMaskIntoConstraints
  5. 特别留意: 带有preferredMaxLayoutWidth特点的控件
  1. CHP/CRP 表明 CH Priority 和 CR Priority

  2. >= 表明最小巨细; <= 表明最大巨细

  3. Intrinsic Width, 表明 最小巨细是width, 最大巨细也是 width, 仅仅Priority不同

4 常见的 CHCR 的装备

func makeImageView(named: String) -> UIImageView {
    let view = UIImageView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.contentMode = .scaleToFit
    view.image = UIImage(named: named)
    // 图画抗紧缩/抗拉伸的经历装备 
    // 答应图画在垂直方向上适配: 被拉伸 or 被紧缩
    // By making the image hug itself a little bit less and less resistant to being compressed
    // we allow the image to stretch and grow as required
    view.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .vertical)
    view.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 749), for: .vertical)
    return view
}

针对 UILabel:

func makeLabel(withText text: String) -> UILabel {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.text = text
    label.backgroundColor = .yellow
    // 文本一般需求更加紧凑!!!
    label.preferredMaxLayoutWidth = 100
    // 开发经历: 与 XIB 相同, 设置CH为251
    label.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .horizontal)
    label.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .vertical)
    return label
}

XIB 或者 Storyboard中, UILabel的CHP是251, UIImageView的CRP是749!!!

其他apple 官方的主张:

  • When stretching a series of views to fill a space, if all the views have an identical content-hugging priority, the layout is ambiguous. Auto Layout doesn’t know which view should be stretched.

    A common example is a label and text field pair. Typically, you want the text field to stretch to fill the extra space while the label remains at its intrinsic content size. To ensure this, make sure the text field’s horizontal content-hugging priority is lower than the label’s.

    In fact, this example is so common that Interface Builder automatically handles it for you, setting the content-hugging priority for all labels to 251. If you are programmatically creating the layout, you need to modify the content-hugging priority yourself.

  • Odd and unexpected layouts often occur when views with invisible backgrounds (like buttons or labels) are accidentally stretched beyond their intrinsic content size. The actual problem may not be obvious, because the text simply appears in the wrong location. To prevent unwanted stretching, increase the content-hugging priority.

  • Baseline constraints work only with views that are at their intrinsic content height. If a view is vertically stretched or compressed, the baseline constraints no longer align properly.

  • Some views, like switches, should always be displayed at their intrinsic content size. Increase their CHCR priorities as needed to prevent stretching or compressing.

  • Avoid giving views required CHCR priorities. It’s usually better for a view to be the wrong size than for it to accidentally create a conflict. If a view should always be its intrinsic content size, consider using a very high priority (999) instead. This approach generally keeps the view from being stretched or compressed but still provides an emergency pressure valve, just in case your view is displayed in an environment that is bigger or smaller than you expected.

参考

  1. github.com/forkingdog/…
  2. developer.apple.com/library/arc…
  3. constraints.cs.washington.edu/solvers/cas…
  4. github.com/SnapKit/Sna…
  5. github.com/layoutBox/P…