布局千变万化,但万变不离其宗。
历史背景
iPhone4s
及以前的设备尺寸都是固定320*480
(为什么这么设计呢?乔帮主认为这是人类手持设备最适合的尺寸),但随着时间推移及技术发展,不同设备尺寸层出不穷。
iPhone5
出来后开发者就要做界面适配了,因为设备尺寸变成320*568
了。Autoresizing
其实主要是为了解决iPad
的界面适配(横竖屏),iPhone也是适用的,但Autoresizing
局限性较大,它只能设置自身和父控件的关系,对于一些复杂的界面根本无法完成 。iPhone5(iOS6)
已经发布了AutoLayout
技术,主要是为了解决移动端屏幕适配问题,比Autoresizing
更丰富,但当时开发工具版本是Xcode4
,对Autolayout
的支持很不友好,再加上需要往下兼容iOS5
甚至iOS4
的系统,所以很少有开发者使用它。
iPhone6
及iPhone6Plus
发布后,开发工具也随之升级到Xcode5
,Autolayout
的开发效率也有很大提升,苹果官方也推荐使用Autolayout
来布局UI界面。因为它可以很轻松解决屏幕适配问题(解决任何控件之间的相对关系),开发者才开始逐步使用Autolayout
。
iOS8
之后又推出了Sizeclasses
,可以理解为AutoLayout
的升级版。但我觉得后面苹果应该会往声明式编程的大前端行进,Flex
布局也会一统天下,到那时界面布局应该会稳定下来吧……
Autoresizing
Autoresizing
其实很简单,因为它只能约束子控件和父控件之间的关系,不能约束兄弟控件的关系,这也是它最大的壁垒。我们先通过Storyboard演示下Autoresizing
是如何使用的,然后再看纯代码的写法。
Storyboard
由于Autoresizing
和AutoLayout
是互斥的,所以如果你的开发工具是Xcode8以下的,需要选择Main.storyboard
后点击右边文件简介区顶部的第一个按钮(Show the File inspector),将Use Auto Layout和User Size Classes两个复选框取消勾选(默认是勾选的)。当取消勾选Use Auto Layout时系统会弹出标题为Using Size Classes Requires Auto Layout的弹框,选择Disable Size Classes即可(此时User Size Classes会自动取消勾选)。Xcode8及以上不需要做任何改动。
案例一(场景:红色view位于屏幕右下角且宽高均为100,根据屏幕自适应位置及尺寸)
添加view
添加完成后预览,在8Plus上展示没有问题,但我们在4s(只是作为不同尺寸屏幕预览而已)上面看到刚才添加的红色view没有展示(也可以通过旋转屏幕查看view是否正常显示)。原因是红色view是相对于8Plus设置的frame,也就是view的x坐标是(414-100=314),y坐标(736-100=636),但是4s的屏幕尺寸是320*480,不在屏幕范围内,所以不显示。
修改Autoresizing
我们看到修改尺寸下面有个Autoresizing
栏目,光标移到对应位置还会有动画效果。左边有一个可操作区(默认连接左边和上边),里面外围每一条线代表距离父视图是固定间距。如果点亮右边和下边的线,意味着该view距离父视图右边和下边都是固定间距。内围有两条线,水平方向代表宽度是否伸缩,垂直方向代表高度是否伸缩。每改变一次连线状态,都能在右边看到预览动画。
选中红色view,取消上和左的连接状态,连接右和下,这时候看到在两个不同尺寸屏幕上都正常显示。
选中水平和垂直,宽度和高度将会根据屏幕自动伸缩。
新增一个蓝色view位于屏幕左下角,宽高和红色view相等。
可以看到,蓝色view和红色view是兄弟关系,但只能设置相对父视图的位置和伸缩尺寸,无法设置兄弟之间的关系。
思考:如果四条线都勾选会怎么样呢?
view会被展示到左上角,因为四条线的相对方向是有互斥性及优先级的,左 > 右,上 > 下。
纯代码
UIView
有一个autoresizingMask
属性可以设置Autoresizing
。autoresizingMask
是一个UIViewAutoresizing
类型的枚举值,默认值是UIViewAutoresizingNone
。
1 | typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) { |
案例一(场景:红色view位于屏幕右下角且宽高均为100,根据屏幕自适应位置及尺寸)
1 | UIView *redView = [[UIView alloc] init]; |
案例二(场景:红色view位于屏幕右下角且宽高均为100;蓝色view右边距离红色view的左边间距为30且宽高和红色view相等)
1 | UIView *redView = [[UIView alloc] init]; |
AutoLayout
如果项目允许可视化编程(Interface Builder
),AutoLayout
必然会被用到,这也是整个IB
的精髓。苹果官方也建议开发者使用AutoLayout
进行UI布局,因为它确实很高效。由于使用代码实现AutoLayout
会非常繁琐,所以我们一般也只在IB
中使用。当然,纯代码方式推荐使用第三方框架Masonry。
AutoLayout
有两个核心概念:
- 约束:通过给控件添加约束,来决定控件的位置和尺寸
- 参照:添加约束时,是依据谁来添加(可以是父控件或者兄弟控件)
自动布局的核心计算公式:obj1.property1 = (obj2.property2 * multiplier) * constant value
,先记住公式即可,接下来我们通过案例来慢慢了解他。
官网指南:Auto Layout Guide
Storyboard
打开Storyboard
,创建一个view并选中,在操作区下方有一个添加约束的按钮,点击之后会出现约束条件,有一个Contrain to margins
复选框,取消勾选(每次都要这样做,为什么?这是苹果官方推荐的边距尺寸,视图和跟控制器的边距是20pt,父子之间是8pt,其实我们没必要使用它)。
添加约束
约束条件上面的四个边距和autoresizing
很相似,默认设置相对于最近一个视图的上下左右边距,下面的就很好理解了,设置宽度和高度。设置完成后一定要点击最后的Add 4 Contraints
,这样才能将约束添加到对应的视图上。
可以用鼠标点击对应数值框修改,也可以按顺序使用键盘Tab
键,每次值变化后都会默认选中对应约束条件。添加约束完成后会自动更新IB界面,Xcode9之前都需要我们手动更新。
约束添加完成,预览发现,不同尺寸或横竖屏展示的效果都一样。
注意:约束不能重复添加,否则会报错。比如我们需要把红色view的宽和高修改为150,直接添加一个宽高150的约束。
系统提示约束错误,我们根据系统提示把需要删除的约束删除即可。
修改/更新约束
1. 删除之前约束,添加新的约束
选中操作元素(即红色view),点击约束区Resolve Auto Layout Issues
按钮。选择Clear Constraints
,这时候就把view的所有约束都清除掉了,添加新的约束即可。
清除约束时,慎重点击Clear Constraints
,该操作的对象是整个根视图内的所有约束。从上图可以看到,系统已经对这些操作做了分组处理,一般情况下我们只需要关注Selected Views
分区就行。
2. 左边栏
展开左栏view的约束,点击需要修改的约束,在右边栏的Constant
输入框内输入需要修改的值,回车。
3. 双击view的约束线条(不建议,触控范围太小)
双击需要修改的约束线条,在弹出框内修改Constant
的值,然后点击回车。
4. 右边栏
通过右侧的尺寸区域修改约束条件,两种选择方式选其一即可(不同方式可修改的条件不一样)。
5. 更新约束
有时候我们在操作界面拖动了已经添加好约束的视图,发现约束线条颜色变黄,而且左上角多了一个警告按钮,这时候只需要把约束更新下就可以了。
6. 约束失效
如果我们需要添加一个新的约束,仅仅是看下效果,又不想删除之前的约束,就可以用这种方法–约束失效。选择待失效的约束,把右边栏的installed
复选框取消勾选(默认都是勾选)。如果想要恢复之前的约束,把installed
再次选中即可。选中约束直接按delete
,约束将会被完全删除。
约束参照
在添加约束的时候,我们可以看到约束值的旁边有一个三角按钮,表示有可选项,有哪些内容呢?他们的作用是什么?
新添加一个view,放到右下角,然后开始添加约束条件,点击右边约束的三角按钮看到默认选中了一个选项Safe Area(current distance = 0)
。意思很简单,当前这个view的约束条件参照物是安全区域(安全区域是刘海屏的概念—即iPhoneX以上系列,简单的说就是空出上面的状态栏和下面的Home指示条,剩余的就是安全区域),并且目前距离安全区域的右边距是0。
例:view展示到屏幕右上角,不同的参照物,可能位置也会不一样。
约束辅助参照对应的还有Top Layout Guide
和Bottom Layout Guide
,在Xcode10以上需要手动把Use Safe Area Layout Guides
取消(默认勾选),才能显示。具体不再介绍,原理和Safe Area
类似。
兄弟之间、父子之间的对齐方式
案例一(兄弟关系-等宽等高)
场景:在Controller中添加两个等宽等高(高度50)的view,左边红色,右边蓝色,父控件左边、右边、下边及连个view之间的间距都是相等的(间距30)。
操作技巧:
- command可以选择多个控件(可以设置等宽等高、对齐方式等);
- 选中控件,按住control,往关联控件内拖就可以设置关联属性;
- 如果设置左右边距的时候发现找不到红色或蓝色view作为参照,很大原因是因为两个在水平方向上没有交集,这时候拖动任意一个到同一个方向即可。
案例二(兄弟关系-中心对齐)
场景:在Controller中添加一个红色view,左右边距和高度都为50,父视图中心点对齐,一个蓝色view,宽度为红色的一半,高度和红色一致,底部距离红色顶部50。
操作技巧:
- 约束添加完成后,如果界面没有变化也没有警告报错,可以尝试主动修改约束的常量值(
Constant
)。
案例三(父子关系-等宽等高及对齐)
场景:4个view进行2*2排列占满屏幕,每个view都有一个距离自己四边距30的子view。
操作技巧:
- 当视图比较多的时候,可以给每个view打上一个标签名,这样就很容易区分;
- 视图约束参照物默认选择距离最近的一个。
案例四(UILabel内容伸缩)
场景:UILabel根据文字多少自动计算宽高。
- 没有
AutoLayout
之前,UILabel
的文字内容总是居中显示,导致顶部和底部会有一大片空白区域; - 有了
AutoLayout
只后,UILabel
的bounds
默认会自动包住所有的文字内容,顶部和底部也没有空白区域。- 这是因为
UILabel
设置约束后会根据内容的多少自动计算需要显示多少行,并且根据字体大小计算每行文字的高度,计算完成后会把内容高度赋值给UILabel
,这时候看到的效果就是UILabel
高度会随着文字的多少自动计算。
- 这是因为
添加一个
UILabel
到控制器上,如果不加任何约束,文字垂直居中水平居左展示。多行文字,也只在
UILabel
的范围内尽可能的展示(需要设置行数为0,即不限制行数),但仍然显示不全。给
UILabel
添加居中显示的约束。UILabel
添加约束后和其他控件不太一样,由于没有设置宽度和高度,Xcode也没有报错或警告,仅仅文字由之前的多行变成了单行显示。这是因为UILabel
没有设置宽度,系统只能根据当前文字大小无限制的展示一行,所以高度也只是一行文字的高。
我们可以看到如果不设置宽度,仅设置居中对齐的话,UILabel
的X坐标和宽都会随着文字的多少而变化,就算超出屏幕也一样。如果我们添加宽度约束,并把约束修改为小于等于300(
Less Than or Equal
),就会发现UILabel
可以正常显示了。
5.1. 添加宽度约束
5.2. 修改宽度并设置约束关系为小于等于
5.3. 可以自动计算高度
思考:为什么设置
Less Than or Equal
?设置其他关系就不可以么?
约束关系共有三种:
Less Than or Equal
小于等于(<=)Equal
等于(=)Greater Than or Equal
大于等于(>=)
Less Than or Equal
的意思是最大值不能超过多少。我们设置了宽度300,如果文字很少的情况下,Label会包裹内容显示;如果文字超过300,就会换行显示。
如果设置为Equal
,意思就是宽度只能是300,文字很少情况下,Label不会包裹内容显示,宽度一直是300;文字超过300时,同样还会换行显示的。
如果设置为Greater Than or Equal
,意思是宽度最小是300,文字很少情况下,Label不会包裹内容显示,宽度一直是300;文字超过300时,也不会换行。
操作技巧:
- UILabel包裹内容三要素:
- 设置约束
- 设置宽度(小于等于或等于)
- 不要设置高度
案例五(父视图随子视图内容伸缩 – 朋友圈/微博)
场景:模拟一条微信朋友圈信息。
查看下面的约束,重点是time的顶部约束等于content的底部约束加上10的间距,根视图view的底部和time的底部对齐,并且有8间距。
修改内容,查看效果
1 | @interface ViewController () |
操作技巧:
- 记住万能公式
约束优先级
如何根据不同情况设置不同的约束?可以设置约束的优先级,系统在IB
中给我们提供了三种优先级选择,纯代码方式提供了7个常量,其实我们都可以设置(0, 1000]
范围内任意值。
特点:优先级值越高,优先生效。
示例
场景:三个view,并行排列(尺寸、间距都相同),移除中间一个view,最后的view自动往前移动到被删除view的位置。
添加三个view并设置约束;
把中间的view移除;
1
2
3
4
5
6
7
8
9@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *middleView;
@end
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.middleView removeFromSuperview];
}移除后,右边的view并没有向前移动。当中间的view被移除后,约束就不存了,右边的view约束又参照于中间的view,所以也没有约束了,最后两个都不显示了;
右边的view添加新的约束(以左边的view为参照)并设置约束优先级;
约束优先级生效。
操作技巧:
- 优先级低的约束,会用虚线框连接或包裹
约束动画
正常线性动画只需要变换frame
即可,但是如果是约束怎么办呢?苹果规定使用约束后在修改控件尺寸和位置就不能使用frame
,应该继续使用约束。
示例
场景:点击屏幕修改view的宽度,动画显示,动画时长持续2s。
添加view并设置约束;
修改宽度约束值;
1
2
3
4
5
6
7
8
9
10
11
12@interface ViewController ()
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *widthConstraint;
@end
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[UIView animateWithDuration:2.0 animations:^{
self.widthConstraint.constant = 150;
}];
}动画没有生效;
使用强制刷新;
1
2
3
4
5
6- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.widthConstraint.constant = 150;
[UIView animateWithDuration:2.0 animations:^{
[self.view layoutIfNeeded];
}];
}动画生效,为什么这样就可以了呢?因为约束的本质也是转化为
frame
,此时修改约束时,我们并不知道什么时候计算frame
,设置layoutIfNeeded
后,等待下次屏幕刷新时就会按照动画形式刷新view。
常见警告和错误
警告:控件的frame不匹配所添加的约束
- 约束和显示不一致
- 例:约束控件的宽度为100,而控件现在的宽度是110
- 解决方案
- 更新约束
- 约束和显示不一致
错误:
缺乏必要的约束
- 例:之约束了宽度和高度,没有约束具体位置
- 解决方案
- 添加缺失的约束条件
两个约束冲突
- 例:1个约束控件的宽度为100,1个约束控件的宽度为110
- 解决方案
- 删除其中一个约束或使其失效
纯代码
AutoLayout
一般用在IB
上,纯代码非常繁琐。如果使用AutoLayout
则必须放弃frame
,因为AutoLayout
的底层实现还是设置frame
,如果设置了frame
会发生很多冲突甚至未知性错误。
AutoLayout
和Autoresizing
是互斥的,但UIView
的Autoresizing
功能是默认开启的,如果想用AutoLayout
布局UI,则必须把对应控件的translatesAutoresizingMaskIntoConstraints
设置为NO。
每一条约束其实就是一个对象,所以用代码创建约束的时候就是创建很多约束对象。
示例
场景:红色view位于屏幕右下角且宽高均为100,根据屏幕自适应位置及尺寸。
1 | /* 添加约束方法:对比万能公式理解 |
约束应该添加到哪个视图上面?
添加约束原则:
- 两个同层级关系的控件,约束添加到他们的父视图上;
- 两个不同层级关系的控件,添加到他们最近的共同父视图上;
- 有层级关系的控件,添加到层次较高的父view上。
对比上面的简单视图使用AutoLayout
的纯代码方式实现起来很繁琐,代码量也很大,即使约束有冲突编译也不会报错。所以,平时开发尽量使用IB。
VFL
VFL(Visual Format Language),翻译为“可视化格式语言”,是苹果公司为了简化AutoLayout的编码而推出的抽象语言。
VFL只有水平方向(H
:Horizontal)和垂直方向(V
:Vertical)的约束,在水平方向上控件名后面的常量是宽度,垂直方向代表高度。
格式
H:[控件名1(宽)]-间距-[控件名2(宽)]
- 示例:
H:[redView(20)]-10-[blueView(30)]
- 含义:水平方向上,控件redView的宽是20,控件blueView的宽度是30,他们之间的间距是10。
- 示例:
H:[控件名(>=宽度@优先级)]
- 示例:
H:[redView(>=20@700)]
- 含义:水平方向上,控件redView的宽大于等于20,该约束条件优先级为700(优先级最大值是1000,优先满足优先级高的约束)。
- 示例:
H:|-间距-[控件名1(宽)]-[控件名2(>=宽度)]-|
- 示例:
H:|-10-[redView(>=20)]-[blueView(30)]-|
- 含义:水平方向上,控件redView的宽大于等于20,距离父控件左间距10;控件blueView的宽是30,紧靠redView的右边,距离父控件右间距为0。
- 示例:
示例
场景:红色view位于屏幕右下角(边距均为20)且宽高均为100,根据屏幕自适应位置及尺寸。
1 | /* 添加VFL约束 |
操作技巧:
如果views的映射一般情况下都和对象的指针名称一致,其实没有必要做映射,使用系统提供的宏定义即可。NSDictionaryOfVariableBindings(...)
,例如:NSDictionary *views = NSDictionaryOfVariableBindings(redView, blueView)
。