Autolayout的第一次亲密接触

 

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下

@property(nonatomic,readonly) NSArray<__kindof NSLayoutConstraint *> *constraints NS_AVAILABLE_IOS(6_0);

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

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

NSLayoutAttributeLeft,        //左边
NSLayoutAttributeRight,        //右边
NSLayoutAttributeTop,        //上边    
NSLayoutAttributeBottom,    //下面
NSLayoutAttributeLeading,    //前面
NSLayoutAttributeTrailing,    //后面
NSLayoutAttributeWidth,        //宽度
NSLayoutAttributeHeight,    //高度
NSLayoutAttributeCenterX,    //X轴中心
NSLayoutAttributeCenterY,    //Y轴中心

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比较棒,属性完美支持
  4. Text views和image views也挺好,在有内容的时候支持,没有内容的时候不支持。这也正是我们想要的

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

- (CGSize)intrinsicContentSize
{
    return CGSizeMake(100, 150);
}

由于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种比较常用的优先级

typedef float UILayoutPriority;
static const UILayoutPriority UILayoutPriorityRequired NS_AVAILABLE_IOS(6_0) = 1000; // A required constraint.  Do not exceed this.
static const UILayoutPriority UILayoutPriorityDefaultHigh NS_AVAILABLE_IOS(6_0) = 750; // This is the priority level with which a button resists compressing its content.
static const UILayoutPriority UILayoutPriorityDefaultLow NS_AVAILABLE_IOS(6_0) = 250; // This is the priority level at which a button hugs its contents horizontally.
static const UILayoutPriority UILayoutPriorityFittingSizeLevel NS_AVAILABLE_IOS(6_0) = 50; // When you send -[UIView systemLayoutSizeFittingSize:], the size fitting most closely to the target size (the argument) is computed.  UILayoutPriorityFittingSizeLevel is the priority level with which the view wants to conform to the target size in that computation.  It's quite low.  It is generally not appropriate to make a constraint at exactly this priority.  You want to be higher or lower.
  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的宽度)

[@[label1,label2] mas_makeConstraints:^(MASConstraintMaker *make) {
    make.width.equalTo(@100).priority(500);
}];

大家猜猜,会怎么样?是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更易懂了。
首先我们看来一段代码

UIView *view = ({
    UIView *view = [UIView new];
    view.backgroundColor = [UIColor grayColor];
    [self.view addSubview:view];
    [view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.insets(UIEdgeInsetsMake(100, 100, 100, 100));
        make.width.equalTo(@300);
    }];
    view;
});
MASAttachKeys(view);

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

2015-12-01 10:04:25.558 TAutolayout[1034:32111] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<MASLayoutConstraint:0x7f8fe9e251c0 UIView:view.left == UIView:self.view.left + 100>",
    "<MASLayoutConstraint:0x7f8fe9e23630 UIView:view.right == UIView:self.view.right - 100>",
    "<MASLayoutConstraint:0x7f8fe9e1c430 UIView:view.width == 300>",
    "<NSLayoutConstraint:0x7f8fe9d306c0 UIView:self.view.width == 375>"
)

Will attempt to recover by breaking constraint 
<MASLayoutConstraint:0x7f8fe9e1c430 UIView:view.width == 300>

Make a symbolic breakpoint at     UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

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

"<MASLayoutConstraint:0x7f8fe9e251c0 UIView:view.left == UIView:self.view.left + 100>",
"<MASLayoutConstraint:0x7f8fe9e23630 UIView:view.right == UIView:self.view.right - 100>",
"<MASLayoutConstraint:0x7f8fe9e1c430 UIView:view.width == 300>",
"<NSLayoutConstraint:0x7f8fe9d306c0 UIView:self.view.width == 375>"

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

<MASLayoutConstraint:0x7f8fe9e1c430 UIView:view.width == 300>

根据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左边。
于是我们写下了代码:

UIView *redView = ({
    UIView *view = [UIView new];
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];
    [view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.top.bottom.offset(100);
    }];
    view;
});

UIButton *button = ({
    UIButton *button = [UIButton new];
    [self.view addSubview:button];
    [button setTitle:@"button" forState:UIControlStateNormal];
    [button mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.offset(0);
        make.top.equalTo(redView);
        make.size.mas_equalTo(CGSizeMake(100, 50));
    }];
    button;
});
MASAttachKeys(redView,button);

运行一下,看看效果:

咦,咋这样了呢?按钮去哪儿了?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,让我们改改代码:

UIView *redView = ({
    UIView *view = [UIView new];
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];
    [view mas_makeConstraints:^(MASConstraintMaker *make) {
        /********修改*********/
        make.left.top.offset(100);
        make.bottom.right.offset(-100);
        /*****************/
    }];
    view;
});

UIButton *button = ({
    UIButton *button = [UIButton new];
    [self.view addSubview:button];
    [button setTitle:@"button" forState:UIControlStateNormal];
    /********修改*********/
    [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    /*****************/
    [button mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.offset(0);
        make.top.equalTo(redView);
        make.size.mas_equalTo(CGSizeMake(100, 50));
    }];
    button;
});
MASAttachKeys(redView,button);

运行一下看看:

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:方法

[UIView animateWithDuration:1 animations:^{
    self.redView.frame = CGRectMake(0, 0, 100, 100);
}];

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

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

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

[self.redView mas_updateConstraints:^(MASConstraintMaker *make) {
    make.width.equalTo(@500);
}];
[UIView animateWithDuration:1 animations:^{
    [self.redView.superview layoutIfNeeded];
}];

当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,就会抛出异常。

2015-12-01 21:41:55.143 TAutolayout[9186:907] *** Assertion failure in -[UITableView layoutSublayersOfLayer:], /SourceCache/UIKit/UIKit-2372/UIView.m:5776
2015-12-01 21:41:55.145 TAutolayout[9186:907] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super.'

注意:直接对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)。
举个例子:

self.view > viewA;
self.view > viewB > viewC > viewD

我们用>符号表示包含关系,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)给出了一些意见:

Really, all this is is a way for views to have a chance to make changes to constraints just in time for the next layout pass, but it’s often not actually needed.
All of your initial constraint setup should ideally happen inside Interface Builder. Or if you really find that you need to allocate your constraints programmatically, some place like viewDidLoad is much better. updateConstraints is really just for work that needs to be repeated periodically.
Also, it’s pretty straightforward to just change constraints when you find the need to do that; whereas, if you take that logic apart from the other code that’s related to it and you move it into a separate method that gets executed at a later time, your code becomes a lot harder to follow, so it will be harder for you to maintain, it will be a lot harder for other people to understand.
So when would you need to use updateConstraints? Well, it boils down to performance. If you find that just changing your constraints in place is too slow, then update constraints might be able to help you out. It turns out that changing a constraint inside updateConstraints is actually faster than changing a constraint at other times. The reason for that is because the engine is able to treat all the constraint changes that happen in this pass as a batch.

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

Reference

Anatomy of a Constraint
Masonry
Masonry介绍与使用实践(快速上手Autolayout)
有趣的Autolayout示例-Masonry实现
何时使用updateConstraints

评论

还没有任何评论,你来说两句吧

发表评论

浙ICP备16008686 -
善始者实繁,克终者盖寡