[TOC]

arrangedSubviews

图中的四个子试图可以通过 arrangedSubviews属性进行设置。

我们可以通过初始化方法进行设置 arrangedSubviews

let arrangedSubviews = [UIView()]
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)

添加一个试图到 arrangedSubviews数组最后。

let addNewView = UIView()
stackView.addArrangedSubview(addNewView)

arrangedSubviews移除指定的试图。

stackView.removeArrangedSubview(addNewView)

我们还可以交换一个试图所在的位置到0

stackView.insertArrangedSubview(addNewView, at: 0)

axis

我们想怎么知道试图是按照横向排列还是纵向排列的呢?我们有一个枚举可以使用。

open var axis: UILayoutConstraintAxis

我们可以通过设置 axis.horizontal为横向布局

stackView.axis = .horizontal

我们可以设置 axis.vertical为纵向布局

stackView.axis = .vertical

现在我们知道怎么设置横向布局还有纵向布局,我们想做下面的试图结构该如何做呢。

红色的试图大小为 375x50,中间的间距是 20。白色的试图大小为 51x33;

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
    override func loadView() {
        let view = UIView()
		view.frame = CGRect(x: 0, y: 0, width: 375, height: 676)
        view.backgroundColor = .white

		let backgroundView = UIView()
		backgroundView.backgroundColor = #colorLiteral(red: 0.5725490451, green: 0, blue: 0.2313725501, alpha: 1)
		backgroundView.bounds = CGRect(x: 0, y: 0, width: 375, height: 50)
		backgroundView.center = view.center
		view.addSubview(backgroundView)

		var arrangedSubViews:[UIView] = []

		for _ in 0 ..< 5 {
			let subView = UIView()
			subView.backgroundColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1)
			arrangedSubViews.append(subView)
		}

		let stackView = UIStackView(arrangedSubviews: arrangedSubViews)
		stackView.frame = CGRect(x: 20, y: 9, width: 375 - 40, height: 50 - 18)
		stackView.axis = .horizontal
		stackView.alignment = .fill
		stackView.distribution = .fillEqually
		stackView.spacing = 20
		backgroundView.addSubview(stackView)

        self.view = view
    }
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

上面的代码可以放在 Playground 看运行的效果。

5441F16D-7ADB-47A9-A422-41132955C892

我们可以在上面的代码中发现了三个起到决定作用的属性分别是 alignment distribution

spacing

alignment

关于 alignment是一个枚举。

fill

fill是铺满的样式。在保证最小间距的情况下,铺满整个 UIStackView。这个模式之下,并不是均等的分割平铺,除非设置下面的属性。

stackView.distribution = .fillEqually

top

9852AED3-1E0A-4CB7-810E-0E7009488FF3

top是基于顶部对其,下面根据再自动约束。

bottom

bottom是基于底部对其,上面再进行自动约束。

对于横向布局来说

leading = top
trailing = bottom

横向布局设置左对齐和右对齐无法生效。

Center

988F0286-3A9B-4A1A-A06A-73E5BFBA4198

center当为横向布局的时候,约束像上下两端延伸。如果是纵向约束的时候,约束像左右两端延伸。

leading

leading当布局方式为纵向排列时候,按照左侧对其。

trailing

trailing是在纵向布局情况下,按照右侧进行布局。

firstBaseLine

firstBaseLine只针对于横向排列布局有效,是针对数组第一个试图,如果不是文本就按照顶端开始对其,是文本按照文本下端对其。

lastBaseLine

510A083F-F423-4558-B970-4BEEC02F904F

lastBaseLine也是针对于横向排列布局有效,是针对数组第一个试图,如果不是文本就按照底部开始对其,是文本按照文本底部对其。

distribution

distribution这个属性是决定子试图布局的最关键因素。

fill

0AF4AB7D-ED07-4631-99EB-969FB5F77876

fill模式是按照设置的间距,之后根据子试图全部铺满整个 UIStackView。在这里我们要提到一个方法那就是 intrinsicContentSize

intrinsicContentSize指代一个试图最适合的大小,对于 UIImageView UILabel UIButton都是可以系统计算 intrinsicContentSize大小的。

但是对于 UIView是系统无法知道 intrinsicContentSize大小。我们就需要子类重写 intrinsicContentSize这个方法让系统知道最适合的试图大小是多少。

我们为什么要知道 intrinsicContentSize的大小,首先我们研究 fill形式试图布局的计算公式。

我们按照上图所以计算

let width = View1.intrinsicContentSize.width + spance + View2.intrinsicContentSize.width + spance + View3.intrinsicContentSize.width + spance + View4.intrinsicContentSize.width

当我们计算出的 Width已经超过我们 UIStackView的宽度的时候,我们会按照优先级,压缩优先级比较低的试图。

如果优先级都一样,那么我们会先保证 arrangedSubviews数组里面排序在前面的试图展示,之后压缩排序在后面的试图。

当我们计算出的 width不足以铺满我们 UIStackView宽度的时候,我们会让优先级别叫高的试图优先按照正确的试图显示,当优先级一样的时候,我们会优先让 arrangedSubviews索引在后面试图优先按照正常尺寸显示,最前面进行拉伸。

这里先讲解一下优先级,优先级分为抗压缩(Content Compression Resistance Priority)还有抗拉伸(Content Hugging Priority)。

Content Compression Resistance Priority

抗压缩的优先级: 750 优先级越高越不容易被压缩。

Content Hugging Priority

抗拉伸的优先级: 250(251) 优先级越高越不容易被拉伸。

这里有个奇怪的问题 代码创建优先级默认为250 但是 XIB 创建的优先级别为251

我们往 UIStackView添加三个 UIView,分别是红蓝黄。

我们发现能可见的试图只有 蓝色的试图,这到底是怎么的回事呢?

因为对于 UIView来说,系统无法计算试图的大小,会返回默认为(-1,-1)

对于显示来说,(-1,-1)是按照 (0,0)计算的。为什么出现蓝色,我也是猜测,但是经过几次实验来说,大概得出来这个结论。

对于 UIStackView布局的数组来说,

如果数组只有一个试图的时候是铺满整个 UIStackView.

如果数组试图大于一个的时候,左侧的边距是最后第二试图和第一个试图间隙想加。显示最后第二个试图。

就按照上面的例子来说。

最后第二个元素是 蓝色的,所以显示出来的是蓝色试图的。和第一个试图只有一个间隙,我们设置间隙最小 10。那么蓝色试图左侧的间隙是是 1*10间距,右侧就是和最后一个的间隙 10

如果只有一个试图的时候。

因为不存在倒数第二个试图,直接显示唯一的一个。一个试图的左侧和右侧也没有试图,那就是没有间距,全部铺满。

为了证明我们 intrinsicContentSize这个值决定 UIViewUIStackView的布局,我们就用自定义 UIView一个类,重写 intrinsicContentSize方法,控制 的大小为(100,0)

为什么我们不设置高度,因为对于横向布局铺满来说。设置高度已经没有什么意义了,布局只关系宽度。

我们设置 UIStackView的宽度为320,间距为10

我们发现我们的三个试图竟然全部的展示出来,因为 100 + 10 + 100 +10 + 100 = 320

我们设置的大小和间距正好等于我们设置 UIStackView的宽度。

我们设置一下 UiStackView小于我们计算的宽度,设置为 280

当我们设置UIStackView 宽度不够的时候,我们的小黄被压缩了。因为要优先的保证间距,三个试图的优先级是一样的,所以小黄的只能委屈的被压缩了。

小黄再想凭什么我就被欺负,小红和小蓝说因为你来的比我们晚。后来小黄因为关系,一下子级别比小红和小蓝都高。没法再欺负了,怎么提升级别,看下面。

因为小黄默认抗压缩级别是750,小黄通过关系提升到1000,让你们还欺负我。

我们从结果来看,我们的小黄自从提升级别之后再也不被欺负,但是小蓝成为下一个挤压。

我们再把小黄的级别降下来,现在让容器变大,宽度变成375

因为 Playground 里面的预览模拟器屏幕的宽度就是375

我们发现小蓝和小黄已经恢复正常了,但是红色竟然被拉长了。这是什么原因呢?

我们想让小蓝拉伸,因为抗拉伸优先级越高越被拉伸,我们设置小蓝如下。

view2.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal)
2ADD4DC7-004C-42FC-A244-FCA611D65B66

我们可怜的小蓝果然变得臃肿了起来,因为 fittingSizeLevel 的优先级为50,是最先考虑被拉伸的。

从上面我们得出一个结论如下:

当我们设置为 fill 模式填充的时候

如果全是 UIView 计算不出来实际大小,就展示倒数第二个试图,如果只有一个就全部铺满

如果宽度不够,就优先压缩优先级比较低的试图,其次从最后一个试图进行压缩。

如果宽度足够,就优先拉伸优先级比较低的试图,其次从最前面的试图进行拉伸。

fillEqually

这个模式是让所有的试图进行平分,如果宽度只够显示最小的间隙就只显示间隙。

这个模式不用计算实际大小,也不用设置优先级。

fillProportionally

这个是按照实际大小按照比例进行分配大小,暂时没找到分配到底有什么规律。

equalSpacing

2F931DF6-A6B3-42DE-A30F-DA0DD851809B

这个模式是按照实际大小布局,如果宽度足够就平均拉伸间距。

如果宽度不足,就按照优先级之后试图顺序进行压缩。

equalCenter

812EFFAE-09C6-4B64-90E1-685051F7B5A6

保证最小间距情况下,按照最适合的大小,之后按照中心点距离相等进行排列。排列出来的间距不一定相等,但是最小必须等于设置的最小间距。

如果宽度小于设置的宽度,那么按照优先级之后按照试图顺序压缩试图。

如果宽度不够,就优先排列宽度最小的一边。如果最小在左侧就从左侧布局

如果最小的在右侧就从右侧布局

isLayoutMarginsRelativeArrangement

是否允许启用边缘布局,默认是 false 全部铺满。

假设我们设置isLayoutMarginsRelativeArrangementtrue,边距都为10。

setCustomSpacing

可以设置某个试图后的间距,这个设置优先级比默认间距优先级高。比如我们设置红色和蓝色中间的间距为5

关于 UIStackView 今天算是研究完,但是文章中有很多不正确的地方,大家发现不正确的地方及时提醒我纠正。