Autolayout 的第一次亲密接触

项目里的布局一直都是纯代码流,顺带着Autolayout也一直没有使用,直到遇到了masonry,让我看到了希望,我决定将Autolayout引入到项目中。masonry的基本用法网上已经很多了,我就先不具体介绍了。大家如果需要了解,可以去看看masonry的demo或者里脊串的Masonry介绍与使用实践(快速上手Autolayout)

masonry只是给Autolayout披了一层华丽的外衣,让他更好用了,但真正实现布局的还是Autolayout,文章主要介绍Autolayout,部分code用的是masonry,但应该不会影响理解。

初识Autolayout

iOS中,会将View的布局定义为一系列的线性方程,存放在UIView的属性中。在View布局的时候,通过这些方程式,计算出每一个view的frame来完成布局。这就是Autolayout。

Constraint

Autolayout将所有的方程式用constraint表示,存放在View的属性constraints下

每一个constraint,表示一个相等或不等(大于小于)关系

上面的这个constraint表示红色view的头部距离蓝色view的尾部8个点。

  • Item: 一般的item都是View
  • Multiplier: 系数,一般对宽高的属性用得比较多
  • Constant: 常量,一般为设置距离,大小什么的
  • Relationship: 关系,上图中表示的是相等关系,除此之外也可以用不等关系表示,例如:>=, <=
  • Attribute: 属性,iOS中主要的属性有上,下,左,右,前,后,宽,高,Y轴中心,X轴中心。

NSLayoutAttributeLeading和NSLayoutAttributeTrailing可能不太好理解。 一般正常情况下,我们的文字顺序是从左到由,所以Label的Leading=Left,Trailing=Right,但是如果有的语言,文字的顺序是从右往左(传说古代的文字顺序就是从右往左),那么就是Leading=Right,Trailing=Left。

由于国际化的关系,Apple推荐使用Leading和Trailing代替Left和Right。但是个人感觉Left和Right比较好理解,而且项目支持的文字也都是从左到右的,所以Left和Right反而用的比较多

IntrinsicContentSize

使用Autolayout之后,一个比较爽的地方就是UIlabel,UIButton, UIImageView有了IntrinsicContentSize的属性。他们可以自己根据内容调整大小,再也不用量宽和高了。设置好位置之后,就让他们自己浪吧,文字有多长就显示多长,图片有多大,就显示多大,真是Very Nice~~ 对于哪些View有IntrinsicContentSize,Apple给了一张表:

  1. UIView和NSView是没有IntrinsicContentSize的。
  2. Sliders只有with有这个属性。 Sliders只能定义width。Sliders的height拥有IntrinsicContentSize(感谢@凸小布,发现了这个问题)
  3. Labels, buttons, switches, text fields比较棒,width和height都有IntrinsicContentSize
  4. Text views和image views也挺好,在有内容的时候支持,没有内容的时候不支持。这也正是我们想要的

从上面我们可以看到UIView是没有IntrinsicContentSize的,如果我们自定义一个View,想要他拥有默认宽高,只需要重写-IntrinsicContentSize方法,即可让其拥有默认的宽高。

由于View只有被addSubview之后才能设置约束,所以一直在为怎么让自定义的View拥有默认Size而烦恼。重写intrinsicContentSize可能是最好的让其拥有默认Size的方法了,感谢@里脊串的指点

对于IntrinsicContentSize,Autolayout又把他分成了2个部分:ContentHugging和CompressionResistance:

  1. ContentHugging我翻译过来是内容凝聚力,表示View的宽度和高度紧靠内容,不让其扩展的力量
  2. CompressionResistance是指压缩阻力,表示当有力量要对其进行压缩的时候,其阻力的大小

对于同一个View,ContentHugging和CompressionResistance不会同时起作用。当一个Label有文字的时候,label会存在一个内容的Size。

如果有外力让其size扩张,ContentHugging会起作用,外力大于ContentHugging的力量,label的size由外力决定,反之,label的Size由内容决定。

如果有外力让其size压缩,CompressionResistance会起作用,外力大于CompressionResistance的力量,label的size由外力决定,反之,label的Size由内容决定。

Priorities

各个约束力量的大小,由constraint的优先级(Priorities)决定,优先级越高,力量越大。系统的优先级由1~1000的数字表示,值越大,优先级越高。NSLayoutConstraint中一共定义了4种比较常用的优先级

  1. UILayoutPriorityRequired: 必须级别优先级,值为最高值1000,一般平时定义约束,默认都是这个优先级。
  2. UILayoutPriorityDefaultHigh: 高优先级,值为750,CompressionResistance的默认优先级是这个。
  3. UILayoutPriorityDefaultLow: 低优先级,值为250,ContentHugging的默认优先级是这个
  4. UILayoutPriorityFittingSizeLevel: 极低的优先级,让系统估算Size的时候使用,不适合做约束

知道了各个属性的默认优先级之后,就可以解释为什么一般情况我们给Lable设置Size约束之后,Label由我们设置的Size决定,而不是由其内容决定。因为我们没有特意设置优先级,用的都是默认优先级。Size约束的优先级比CompressionResistance和ContentHugging的优先级高。如果我们想让Label由内容决定,我们可以不设置Size约束或者调低自己Size约束的优先级。

有了优先级之后,我们就可以处理很多复杂情况了。比如2个Label排列在一起,宽度都由内容决定,父view宽度不够的时候,我们需要优先显示某个Label的内容。这时候我们就可以设置2个Label的CompressionResistance优先级, 优先级高的Label,会优先显示~~~

更多例子可以看土土哥的有趣的Autolayout示例-Masonry实现

IntrinsicContentSize举例(12月6日新增)

由于IntrinsicContentSize的ContentHugging和CompressionResistance比较抽象,很多人没怎么看明白。所以举几个例子,帮助大家理解。(感谢@里脊串的提醒)

假设有2个Label,并列放着,他们都是使用IntrinsicContentSize自动根据文字适应宽度。效果如图所示:

一、那么我们设置一个优先级为500宽度为100的约束(100小于Label2的宽度,大于Label1的宽度)

大家猜猜,会怎么样?是2个Label都变成100的宽度,还是都保持原来的宽度不变?还是一个变成100,一个保持原来的宽度? 我们Run一下:

咦!Label1变成了100,Label2还是原来的宽度,为什么呢?

  1. Label1的IntrinsicContentSize宽度比100小,所以当添加一个宽度为100的约束时,ContentHugging在起作用。ContentHugging的优先级为250。宽度为100的约束优先级为500大于ContentHugging。所以宽度为100.
  2. Label2的IntrinsicContentSize宽度比100大。所以当添加一个宽度为100的约束时,CompressionResistance在起作用,CompressionResistance的优先级为750。宽度为100的约束优先级为500小于CompressionResistance。所以宽度还是IntrinsicContentSize的宽度。

根据这个例子,大家应该能明白ContentHugging和CompressionResistance是什么意思了吧。这里留个问题给大家,如果设置的不是宽度为100,而是Label1宽度等于Label2宽度,那么会出现什么情况?是都变成Label1的宽度了,还是都变成Label2的宽度了?还是不变?

调教Autolayout

Autolayout的动态布局虽然感觉很酷炫,但是真正用起来可能会遇到各种问题:动不动就抛了个异常,一不小心就布局冲突了。布局完成之后,突然有个View不见了,View的位置完全不正确等等。这一点也正是一直被人嫌弃的地方。

Autolayout就像一个长得漂亮但性格暴躁的姑娘,需要我们好好调教,才能成为一个合格的女票~~

Log

写布局的时候我们经常会遇到布局冲突,一般冲突都会有抛出log。 下面我们是以Masonry为例看看Log,masonry重写了constriant的-description方法,让log更易懂了。 首先我们看来一段代码

一个View被加在了self.view上,他的上下左右都距离父View100,然后我们又让他的宽度等于300。我们运行一下看看~

唉妈呀,出现了这么大一堆log。 Log说,我们不能同时满足下面的约束,你写的这些约束里面肯定有某个约束有问题,你好好改改:

我们这次的运行结果是把这条约束干掉了:

根据Log的信息,肯定是约束有地方冲突了,而且重点还是宽度相关的约束,因为运行的时候把UIView:view.width == 300干掉了。让我们来看看这些约束:

  • view的左边 = self.view左边+100
  • view的右边 = self.view右边-100
  • view的宽度 = 300
  • self.view的宽度 = 375

根据上面的约束关系,view的左边右边的都是参照self.view的,那么view的宽度应该是375(self.view宽度) – 100(左边) -100(右边) = 175。而我们又给view的宽度赋值了300。所以这个地方冲突了。

Ok,我们把make.width.equalTo(@300);这句话干掉,再次运行一下。Nice,已经没有冲突了~

Visualizing Views and Constraints

冲突问题根据log搞定了,不过你以为这样就完事了么?那就too young too simple了。有的时候我们写完布局,以为一切ok了,一运行,唉妈呀,咋这样啦?也许是view不见了,也许是view布局不对了,反正有可能是各种摸不着头脑的问题,感觉明明是对的,一运行就错了。

对于这一类布局和期望不一致的问题。我们还有一个大招,使用Xcode查看布局的工具

我们想要一个redView,在self.view里面包着,并且距离self.view的边框上下左右均为100。还有一个button,靠着self.view左边。

于是我们写下了代码:

运行一下,看看效果:

咦,咋这样了呢?按钮去哪儿了?redview也没有居中?检查一下代码….没问题呀!!!!难道是是手机出bug了?要不要摔一下试试? 还是不要轻易摔手机,我们来看看View的布局。点击查看View层级的按钮。(Xcode下方,代码和控制台的中间)

出来了view的层级视图,大家可以看到,这个地方就是我们的button,位置并没有错

点击一下,我们还可以看到他的文字。哎呀,原来是我们忘了给他的文字调整颜色了,背景是白色,文字也是白色,所以被”隐藏”了。 那我们的redView是怎么回事呢?

点击一下工具栏最左边的按钮,下图红色框起来的就是工具栏。

这个按钮会显示出超出屏幕外的视图

我们看到,原来他们的相对位置被设置成这样了,那到底是哪里的设置出了问题呢? 点击工具栏左边第二个按钮,这个按钮可以显示出布局的约束。

我们可以看到: self.right = superview.right + 100 self.bottom = superview.bottom +100 原来+100是往右往下,我们要让redView被self.view包着,并距离left,bottom为100,需要用-100。 ok,让我们改改代码:

运行一下看看:

very Good! 跟我们期望一致了

Autolayout深层次的探索

Autolayout什么时候计算frame

根据最初的介绍,Autolayout是在设置constraint的时候,将constraints存放在View的属性中,在真正布局的时候去计算出view的frame,完成布局。那么他到底是在哪个方法中进行计算的呢? 我们知道,计算出来结果之后,必定会改变View的位置。由于Autolayout不通过frame布局,而是直接设置center和bounds。我们给-setCenter:打一个断点。

通过断点,我们可以看到在layoutSubview的时候,如果使用了约束会调用_updateConstraintsAsNecessaryAndApplyLayoutFromEngine。在这个方法里面,系统会先看看是否需要更新约束,如果需要,则调用-updateConstraints更新约束。

跟新结束之后,会调用到_resizeWithOldSuperviewSize:,根据这个方法名,我们可以猜到,是在这个方法里面根据约束,计算出来布局的位置。

计算完成之后调用_applyISEngineLayoutValues。应用布局,调整center和bounds。

Autolayout做动画(12月3日新增)

对于Autolayout,有一个问题就是怎么做动画? 使用Frame的时候,我们做动画一般都调用-animateWithDuration:animations:方法

在animations的block里面调整Frame即可,使用Autolayout之后,由于Autolayout是延迟布局的,并不是约束更新之后就立刻布局,所以大家可以发现。在-animateWithDuration:animations方法里面修改约束是不能实现动画的。

实现动画的关键在于把更新Frame的操作在block中调用。根据前面我们知道,Autolayout是在父view的-layoutSubview中更新Frame的。我们只需要让父View的-layoutSubview方法在block中执行即可。

查阅文档我们知道,iOS中不建议直接调用-layoutSubview,如果要更新布局。可以调用-layoutIfNeeded。调用-layoutIfNeeded之后,会同步执行-layoutSubview。 所以如果我们要做动画可以用下面这种方法:

当View约束发生变化时,是怎么调整布局的

当一个view的约束发生变化的时候,他又是怎么响应调整父view自身以及子view的布局的呢? 我们现在有3个view: view1, view2, view3

当因为某种原因,view2的约束发生了变化,我们来看看会发生什么:

  1. view2由于自身约束发生了改变,需要重新布局。会调用父view:view1的setNeedLayout。告诉View1,我需要重新布局了,赶紧调用layoutSubviews
  2. view1根据已有约束,看看自身布局是否需要改变,如果需要改变,则继续调用父view的setNeedLayout。如果不需要改变,直接调用自己的layoutSubviews
  3. 在view1的layoutSubviews中,完成了view2的布局,这时候view2的布局发生了改变,继续调用view2的layoutSubview
  4. 在view2的layoutSubview中,view3的布局没有发生改变,所以不需要继续调用layoutSubview,结束

在Autolayout下使用Frame

在Autolayout下使用Frame分为2中情况

  1. Autolayout生效之前使用frame。这种情况比较常见,比如在viewDidLoad中对一个view添加了约束,之后又通过Frame调整他的位置。 这种情况下,通过Frame调整位置的代码是无效的。因为在真正布局显示到频幕上的时候,系统会根据约束,重新计算Frame,之前设置的Frame会被冲掉
  2. Autolayout生效之后,使用frame。这种情况稍微少一些,比如View之前就设置了约束,点击某个按钮,需要改变View的Frame。这时候不使用约束,直接setFrame: 这种情况下,setFrame:是可以生效的,不过由于是直接setFrame,不是根据约束计算的。所以他的子View,父View,以及同级的约束依赖的View,都不会跟着改变。而且如果他的superView被触发了layoutSubviews,又会自动根据约束设置成约束的Frame,后患无穷。所以一个View使用约束之后,强烈建议不要再对他使用frame。

第二种情况下,如果是需要做一个动画,动画结束后,又会恢复到原有位置。可以使用Frame

Autolayout的小情绪

Autolayout虽然好用,但是有的时候会有一些小情绪,特别在iOS6上。那时候Autolayout还不完善。

UITableView的layoutSubviews没有调用[super layoutSubviews]

在iOS6上,UITableView的-layoutSubviews中没有到调用UIView的layoutSubviews。 根据前面的介绍,Autolayout自动布局是在UIView的layoutSubviews中,所以TableView上的子view(如:cell,headerView,footerView)使用了Autolayout,tableView在布局的时候调用layoutSubviews,就会抛出异常。

注意:直接对TableView使用Autolayout是不会有问题的,TableView是否调用layoutSubviews在于他上面的子view是否使用Autolayout,而不是他本身。详细原因见当View约束发生变化时,是怎么调整布局的

解决方案:

如果是cell,我们经常使用[cell addSubview:view]再对view做一个相对cell的约束,这时候就会出现问题。解决方案就是使用[cell.contentView addSubview:view]。我们约束是对cell的contentView添加,跟cell无关。tableView就不会调用layoutSubviews了

如果是headerView或者footView。解决方案是直接使用frame,或者自己定义一个类似Cell的contentView的view,子view相对contentView布局使用Autolayout,contentView对headerView布局使用frame

ScrollView的相对布局

scrollView是一个特殊的View,因为他除了位置大小之外还有content内容。scrollView的attribute分为2种:

  1. width,height,center用来表示scrollView的frame。
  2. edge 和 margin用来表示scrollView的content

所以在scrollView布局的时候,如果想让contentSize跟着里面的子view变化。一定要将edge设置完整。当然直接设置ContentSize也是可以的

不同性质的Attribute不能参照

Autolayout中,不同性质的Attribute是不能参照的,就像你不能设置View1的left距离view2的top为10像素。这明显是不合适的,因为left和top是不能对比的。

那么问题就来了,哪些东西可以和哪些东西对比呢?我整理了一张表

View不在同一坐标系统下不能参照

所谓同一坐标系统,是指他们是否能找到共同的父view(我们把父view的父view也称为父View)。 举个例子:

我们用>符号表示包含关系,viewA和ViewB都是self.View的子view。ViewC是ViewB的子View,ViewD是ViewC的子View。ViewA和ViewD是可以相互参照的,因为他们能找到共同的父View:self.View

在View没有被addSubView之前。他是不能跟其他View做对比的。因为他跟任何的View(他自己的子view除外)都找不到共同的父view,也就是说他跟任何View都不在同一的坐标系统下。

何时使用updateConstraints

使用Autolayout之后,系统中多了一个更新约束的方法updateConstraints。看这个方法名,在自定义View的时候,是不是把约束相关的代码放这里面会更好一些呢? 2015年的WWDC技术讲座Mysteries of Auto Layout (Part 2)给出了一些意见:

简单总结一下就是: 初始化constraint的代码放在viewDidLoad等初始化方法中更好。 updateConstraints方法仅用于提升性能。当你更新大量约束,发现由于约束太多,布局有点卡。这时候你可以使用updateConstraints,因为在updateConstraints中更新约束会批量操作,能获得更好的性能(一般不会遇到这种情况)

所以正常情况下,我们直接在初始化的方法中写约束就好。详细资料参考何时使用updateConstraints

Reference

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

1 1 收藏 评论

关于作者:小笨狼

iOS开发者,目前在百度,iOS的QQ群:159974494 个人主页 · 我的文章 · 3 ·      

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部