iOS开发Autolayout进阶


一直想写又不想写。因为近来网上关于Autolayout的文章不在少数,重复相同的内容没什么意思。但是只写不同的地方感觉我也写不出多少,比较心塞。既然坑都占了,那就先意思一下,接下来发现Autolayout其他特性或比较高深的用法会再继续补充。

自iOS6苹果就推出了Autolayout,但那时候很多人都在用AutoresizingMasks,国内对新鲜出土的技术并没有那么热衷,最主要的是改变终归是痛苦的,所以鲜有应用。iOS8之后新推出了iPhone6、iPhone6+及Sizeclass,这时候不得不使用Autolayout了。现在觉得这其实也并不是必须的,代码适配也不是不可以。

现在代码添加约束一般有三种,常规的约束语法、可视化格式语言约束、使用第三方组件。

常规约束

代码基本长这样

高度约束(添加到yellowView身上)

1
2
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:50];
[yellowView addConstraint:heightConstraint];

间距约束(添加到self.view身上)

1
2
3
4
5
6
7
8
CGFloat margin = 20;
[self.view addConstraints:@[
[NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:margin],

[NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant: - margin],

[NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant: - margin]
]];

用到了一个基本公式

1
obj1.property1 = (obj2.property2 * multiplier)+ constant value

multiplier和constant 是向量系数和偏移量,一般还要设置translateAutoresizingMaskIntoConstraints为NO,默认是YES,使得不和系统的自动伸缩约束冲突

可视化格式语言约束

可视化格式语言,即Visual Format Language(VFL),是为了简化Autolayout而推出的抽象语言。

基本长这样

1
2
3
4
5
6
UIButton *b1 = ...
UIButton *b2 = ...
UIButton *b3 = ...
b1.translatesAutoresizingMaskIntoConstraints = NO;
b2.translatesAutoresizingMaskIntoConstraints = NO;
b3.translatesAutoresizingMaskIntoConstraints = NO;

将传入的对象引用作为value,将引用名变成字符串作为key,生成的字典如: {@”b1”:b1,@”b2”:b2,@”b3”:b3}

1
NSDictionary *diction1 = NSDictionaryOfVariableBindings(b1,b2,b3);

为vfl式子中的一些特殊含义的数字做一个名称对照表

1
NSDictionary *diction2 = @{@"top":@20,@"left":@20,@"right":@20,@"spacing":@10};

1
2
3
4
5
6
NSString *hVFL = @"|-left-[b1]-spacing-[b2(b1)]-spacing-[b3(b1)]-right-|";
NSArray *cs1 = [NSLayoutConstraint constraintsWithVisualFormat:hVFL options:NSLayoutFormatAlignAllCenterY metrics:diction2 views:diction1];
NSString *vVFL = @"V:|-top-[b1]";
NSArray *cs2 = [NSLayoutConstraint constraintsWithVisualFormat:vVFL options:0 metrics:diction2 views:diction1];
[self.view addConstraints:cs1];
[self.view addConstraints:cs2];

部分使用规则如下:

|: 表示父视图

-:表示距离

V: :表示垂直

H: :表示水平

>= :表示视图间距、宽度和高度必须大于或等于某个值

\<= :表示视图间距、宽度和高度必须小宇或等于某个值

== :表示视图间距、宽度或者高度必须等于某个值

|-30.0-[view]-30.0-|: 表示离父视图 左右间距 30

[view(200.0)] : 表示视图宽度为 200.0

|-[view(view1)]-[view1]-| :表示视图宽度一样,并且在父视图左右边缘内

V:|-[view(50.0)] : 视图高度为 50

V:|-(==padding)-[imageView]->=0-[button]-(==padding)-| : 表示离父视图的距离
为Padding,这两个视图间距必须大于或等于0并且距离底部父视图为 padding。

[wideView(>=60@700)] :视图的宽度为至少为60 不能超过 700

这里只是基本的部分使用规则,要了解更多请自行查阅。

使用第三方组件

UIView+Autolayout下载地址:https://github.com/ChinaFishNews/UIView-AutoLayout.git

Masonry下载地址 : https://github.com/ChinaFishNews/Masonry.git

在最初的时候使用的是UIView+Autolayout,后来发现Masonry就基本使用Masonry了,这两个用起来都很好上手。

UIView+Autolayout一般用法

直接设置大小

1
[_redView autoSetDimensionsToSize:CGSizeMake(200, 200)];

设置除顶部距四周的间隔,然后设置高度

1
2
3
[_redVeiw autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsMake(20, 20, 20, 20)excludingEdge:ALEdgeTop]; 

[_redView autoSetDimension:ALDimensionHeight toSize:100];

直接设置四周间隔

1
[_redView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsMake(0, 0, 0, 0)];

Masonary一般用法
1
2
3
4
5
6
7
8
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {

make.top.equalTo(superview.mas_top).with.offset(padding.top); make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

或者直接这样

1
2
3
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}]
;

1
2
3
4
5
make.top.mas_equalTo(42);
make.height.mas_equalTo(20);
make.size.mas_equalTo(CGSizeMake(50, 100));
make.edges.mas_equalTo(UIEdgeInsetsMake(10, 0, 10, 0));
make.left.mas_equalTo(view).mas_offset(UIEdgeInsetsMake(10, 0, 10, 0));
1
2
3
4
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
self.topConstraint = make.top.equalTo(superview.mas_top).with.offset(padding.top);
make.left.equalTo(superview.mas_left).with.offset(padding.left);
}]
;

这只是随便举例,想更详细的了解可以到Github源码处具体学习下。

以上,代码添加约束总体较为繁琐,然而有些时候布局比较繁杂、不方便在Storyboard添加的只能只用代码来添加约束了。现在对我而言基本是一般约束直接在Storyboard上拖拽添加,一些不太方便的需要代码添加约束的使用Masonary。

在Storyboard上添加约束

iOS9之后已经可以使用引用了,把Storyboard拆分成多个。多人合作项目的时候经常会产生冲突,Storyboard的冲突改起来比较酸爽,Sotryboard其实是一个巨大的XML文件,之前每次产生冲突都需要以XML文件的形式打开Storyboard修改冲突。即便现在可以把Storyboard拆分成多个,但有时冲突依然不可避免,XML文件其实也并不可怕,毕竟多改几次就习惯了。

添加约束其实比较降低效率,随着布局的繁杂效率下降得更为明显,只在在硬件提升的同时一般我们体会的并不明显。iOS9之后新加了UIStackView,一个管理视图的容器,这样只需要对外面的这个容器添加约束,里面的只需要设置一些属性,虽然归根结底还是设置约束。虽然UIStackView是iOS9之后,但是网上已经有支持iOS9以下的框架,叫FDStackView,下载地址:
https://github.com/ChinaFishNews/FDStackView.git

下载后导入项目即可,无需做其他配置,即可在iOS9以下使用StackView。

先上个图大家感受一下Autolayout

默认大家都已经可以基本使用Autolayout.

一般在右下角可以方便的添加约束,也有人通过菜单栏中Editor设置,其实也可以按住control键,点击控件拖拽

添加约束后有时想知道在不同设备上的布局,这时候可以选中控制器,改变Size的大小,也可以这样

按住option键点击Main.Storyboard

然后可以添加设备

还有时候想观察视图的层次机构,可以在运行时点击Debug View Hierarchy

其实还可以使用工具Reveal,之前的博文中有讲到。

在添加约束的时候,左侧一般是这样的

为什么有的约束在这个控件下,有的约束在其他控件下呢? 是因为只有设置当前控件宽高的时候约束才会出现在当前控件下,设置和其他控件相对位置、距离的时候,会找到他们最近的父控件,在他们最近的父控件下显示约束。

使用Autolayout的时候,有个属性IntrinsicContentSize,UILabel,UIButton,UIImageView拥有这个属性,可以自己根据内容调整大小,不用设置宽和高。文字有多长就显示多长,图片有多大,就显示多大,对于哪些View有IntrinsicContentSize,苹果给了一张表:

这张表显示,UIView和NSView是没有IntrinsicContentSize的。Sliders只能定义width。Sliders的height拥有IntrinsicContentSize,Labels, buttons, switches,and text fields完全支持,Text viewsand image views,在有内容的时候支持,没有内容的时候不支持。对于没有这个属性的,我们也希望他拥有默认宽高的话该怎么办呢?我们可以重写IntrinsicContentSize方法,这样就可以拥有默认的宽高

1
2
3
4
- (CGSize)intrinsicContentSize
{
return CGSizeMake(100,100);
}

对于IntrinsicContentSize,Autolayout又把他分成了2个部分:ContentHugging和CompressionResistance。我们点击控件的时候在右侧会发现

ContentHugging是内容凝聚力,优先级是250,表示View的宽度和高度紧靠内容,不让其扩展的力量

CompressionResistance是指压缩阻力,优先级是750,表示当有力量要对其进行压缩的时候,其阻力的大小

对于同一个View,ContentHugging和CompressionResistance不会同时起作用

而我们添加约束的时候是这样的,优先级是1000

当一个label有文字的时候,label会存在一个内容的Size。
如果有外力让其size扩张,ContentHugging会起作用,外力大于ContentHugging的力量,label的size由外力决定,反之,label的Size由内容决定。
如果有外力让其size压缩,CompressionResistance会起作用,外力大于CompressionResistance的力量,label的size由外力决定,反之,label的Size由内容决定。

也就是说如果我们给label加了一个宽度约束,line设为0,我们在给line设置内容的时候,一旦内容size大于其原有大小的时候label的高度就会增加,而当我们只给label添加一个宽度的时候,一旦我们设置的内容size大于其原有宽度的时候label的宽度就会增加。而当我们同时设置宽度和高度约束的时候,无论我们怎么设置内容,其就会被固定在我们设置的这个size里,因为添加约束的优先级默认是1000,优先级最大,默认会执行这个。

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

1
2
3
4
5
6
7
8
9
typedef float UILayoutPriority;

static const UILayoutPriority UILayoutPriorityRequired NS_AVAILABLE_IOS(6_0) = 1000;

static const UILayoutPriority UILayoutPriorityDefaultHigh NS_AVAILABLE_IOS(6_0) = 750;

static const UILayoutPriority UILayoutPriorityDefaultLow NS_AVAILABLE_IOS(6_0) = 250;

static const UILayoutPriority UILayoutPriorityFittingSizeLevel NS_AVAILABLE_IOS(6_0) = 50;

UILayoutPriorityRequired: 必须级别优先级,值为最高值1000,一般平时定义约束,默认都是这个优先级。

UILayoutPriorityDefaultHigh: 高优先级,值为750,CompressionResistance的默认优先级是这个。

UILayoutPriorityDefaultLow: 低优先级,值为250,ContentHugging的默认优先级是这个

UILayoutPriorityFittingSizeLevel: 极低的优先级,让系统估算Size的时候使用,不适合做约束

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

上面参考了网上的一篇文章,讲的很赞,并且给了一个案例。

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

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

结果是2个Label都变成100的宽度,还是都保持原来的宽度不变?还是一个变成100,一个保持原来的宽度?我们Run一下:

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

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


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

正常情况下我们在设置Label的时候,给它加个宽度约束,运行是这样的

款读设置的是100,高度没有设置,内容过多的时候回自动自适应,不用使用代码计算size。但是我们设置宽度是100,但是内容太少的时候右边会有空白,这时候怎么办呢?我们可以将宽度设为less than or equal 100,这样在内容少的时候就是这样的

这样就解决了内容少而右边有空白的问题。有时候可能没有内容,但是我们又希望它占位,这时候该怎么办呢?我们可以这样设置,即便在没有内容的时候依然在占位

使用的过程中被还会有一些其他的问题

  • 对于Autolayout,制作动画和以往也有些不同,使用Frame的时候,我们做动画一般都调用-animateWithDuration:animations:方法,在animations的block里面调整Frame即可,使用Autolayout之后,由于Autolayout是延迟布局的,并不是约束更新之后就立刻布局,所以大家可以发现。在-animateWithDuration:animations方法里面修改约束是不能实现动画的。而使用Autolayout该怎么做呢? 应该是在block外改变约束,在block内调用layoutIfNeeded方法。

  • Autolayout自动布局是在UIView的layoutSubviews中,所以TableView上的子view(如:cell,headerView,footerView)使用了Autolayout,tableView在布局的时候调用layoutSubviews,就会抛出异常。直接对TableView使用Autolayout是不会有问题的,TableView是否调用layoutSubviews在于他上面的子view是否使用Autolayout,而不是他本身。
    正确的做法是:如果是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。

  • 使用中还会发现一个特殊的控件,UIScrollView,因为contentSize跟着里面的子view发生变化。正确的做法是给UIScrollview添加一个ContentView,设置edge均为0,如果垂直滚动那就设置一个宽度,反之设置一个高度约束。

  • 使用Autolayout之后,系统中多了一个更新约束的方法updateConstraints。updateConstraints方法仅用于提升性能。当你更新大量约束,发现由于约束太多,布局有点卡。这时候你可以使用updateConstraints,因为在updateConstraints中更新约束会批量操作,能获得更好的性能。

以上,只是Autolayout部分用法,具体的需要在项目中体验,注意点比较多。

就先到这里,今天妇女节,祝广大女同胞节日快乐。觉得应该放半天假,然而这么人性化得公司毕竟不多!


如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!

Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!

微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见

老司机iOS周报,欢迎关注或订阅

刚刚在线工作室,欢迎关注或提出建设性意见!

刚刚在线论坛, 欢迎踊跃提问或解答!

如有转载,请注明出处,谢谢!

本站总访问量 本文总阅读量