AutoLayout(I):忘掉Frame,拥抱Constraint

自从iPhone6和6plus出了之后,可以说iPhone进入到了大屏时代。在小屏的时代,常常有很多人是所谓的代码控,有的非常排斥IB这类做法,说什么效率问题。我从开始学习OC写UI,其实只写过4个多月的代码写UI,后来进入第一家公司,公司里面的UI全部是IB,从那时起,我就一直是IB写UI。我自己从来没有感觉到IB有什么不好的地方,效率也没有传说中那么多的问题。更为重要的是,用IB使我的工作效率大大提升,所见即所得。如今这个年代,如果你还坚持用代码写UI,那么恭喜你,你“too young”了,你有适配不完的屏幕。

苹果在iOS6的时候提供了autolayout-自动布局,在iOS8时候提供了class size。目前由于大多数的开发还是要兼容iOS6(iOS5真的没有必要去兼容了),所以在做适配的时候还是只能用autolayout,公司最近在做iPhone6的适配,之前的完全是用代码写死的,这次的适配也等于是基本上重做了。虽然之前简单的知道一些constraint,但是具体用到项目里面还是头一次,折腾了一些,也遇到了一些坑。

忘掉Frame,拥抱Constraint

AutoLayout的第一条信条就是忘记Frame,在传统的写View大小我们可以用Frame来做,但是在AutoLayout下,Frame已经不能满足我们的需求了,应当用constraint来确定View的大小。

raywenderlinch上有两篇写AutoLayout的文章:

http://www.raywenderlich.com/50317/beginning-auto-layout-tutorial-in-ios-7-part-1

http://www.raywenderlich.com/50317/beginning-auto-layout-tutorial-in-ios-7-part-2

感兴趣的同学可以进去看看。这篇blog,会以一个demo的形式铺开。

IB加约束

新建一个工程,命名为AutoLayoutDemo。
可以看到新建工程的目录结构是这样的。

点开main.stroyboard,可以看到在Xcode6后默认的view大小是600x600。首先这个不符合我们的审美,我们更希望它的尺寸和我们的手机屏幕一般,这样比较直观。我们点击viewController,在右侧的Simulated Metrics选项中选择size为iPhone4-inch(也可以根据自己的审美选择4.7inch或者其他的,最好在一个项目中能够统一)。

这个时候view就是4inch了。我们在注意Interface Builder Document这个栏目下,确保勾选中的Use Auto Layout选项,但是不能勾选Use Size Classes(因为要兼容iOS6和iOS7)。

初加约束

拖入一个UIView到ViewController.view上,设置width,height==200,backgroundColor为greenColor。与ViewController进行连线,并且命名为demoView。 可以看到demoView的约束:

下面是约束的种类,列了一下并且做简单解释:

Leading Space to:Superview 相对父视图保持左对齐
Trailling Space to:Superview 相对父视图保持右对齐
Top Space to:SuperView 相对父视图顶部对齐
Bottom Space to:SupderView 相对父视图底部对齐
Width:自身约束宽
Height:自身约束高
Width Equally:view   和参考的view等宽
Height Equally:view  和参考的view等高
Baseline:view 和参考的view在同一水平线
Horizontal Space:view 和参考的view保持水平距离
Vertical Space:view 和参考的view保持垂直距离
Aspect Ratio: 保持宽高比例(暂时我还没有用到,不确定怎么用)

我们点中self.demoView,按住Ctrl键不动进行连线,可以注意到我们往不同的方向连线,系统提供的约束是有区别的。进行约束
完成约束:self.demoView在superView左对齐44像素,自身的宽高约束为200像素,距离superView的顶部92像素。这样的话self.demoView不能在何种大小的屏幕下总是和superView左对齐44像素,上对齐92像素,自身宽高200像素。

view之间的约束

constraint包含了Frame确定尺寸大小的功能但是又比Frame多了一个能够表述不同view之间的相互位置关系的功能。现在我们再建一个view,命名为libView。具体的需求是:让libView在垂直方向上和demoView对齐,并且libView的顶部始终与demoView保持30像素,libView的高度为40,宽度和demoView保持一致。

我们看到图中有黄色的线条,黄色的线条表示warning,红色的线表示约束错误,蓝色的表示约束正确。如果是红色的需要进行调整否则会可能会crash。
上图中遇到的黄色我们可以左侧的黄色按钮查看具体原因。这个时候我们只要将x和width调整为60和200就不会报warning了。跑一下,可以看看效果。

constraint也是可以成为property的

每一个constratint实际是NSLayoutConstraint的类,和其他的OC里面的类一样,也是继承自NSObject。上面的例子中我们把demoView的宽度约束为200,但是现在的需求是将宽度改为300。我们可以在脑海中想象一下这个布局,demoview的宽度会变为300,libView由于有Width Equal的约束,将会导致libView的宽度也会变成300。下面我们来具体实践一下:
将demoView的Width constraint连线到ViewControll.h里面作为一个property,并且命名为demoViewWidthConstraint。

打开ViewController.m文件,在ViewDidLoad里面:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.demoViewWidthConstraint.constant = 300;
}

run一下:

可以看到,demoView的宽度变成了300,并且libView也相应的变成了300,和之前的预期一样。

用代码写约束

IB的本质上是一种持久化的文件,它不具有继承等关系。我们可以这样理解,所有IB能够做的,代码一定可以做的。同样的,对于约束,我们可以用代码实现。
现在的需求是:建立一个view,让他距离self.view的顶部30像素,距离self.view的左边30,距离self.view的右边50像素,自身高度为40。

constraintWithItem写约束

NSLayoutConstraint提供了:

+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;

的方法,该方法的可以转化为一下计算公式:

view1.attr1 = view2.attr2 * multiplier + constant

可以翻译为:view1的某个约束是view2的某个约束乘以变量参数+约束值 得来的。

我们实际写出上面提出的需求,代码在ViewDidLoad里面如下:

//构造一个orangeColor的view
UIView *codeView = [[UIView alloc] init];
[self.view addSubview:codeView];
codeView.backgroundColor = [UIColor orangeColor];

codeView.translatesAutoresizingMaskIntoConstraints = NO;//防止与autosize冲突,一定要写,否则不能正常进行

//距离superView的顶部30像素 codeView的attributeTop约束等于superView的arttributeTop30像素
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:codeView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:codeView.superview attribute:NSLayoutAttributeTop multiplier:1 constant:30];
//距离superView的左边30
NSLayoutConstraint *leadConstraint = [NSLayoutConstraint constraintWithItem:codeView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:codeView.superview attribute:NSLayoutAttributeLeading multiplier:1 constant:30];
//距离superView的右边50像素
NSLayoutConstraint *trailConstraint = [NSLayoutConstraint constraintWithItem:codeView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:codeView.superview attribute:NSLayoutAttributeTrailing multiplier:1 constant:-50];
//自身高度为40
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:codeView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:40];
[self.view addConstraint:topConstraint];
[self.view addConstraint:leadConstraint];
[self.view addConstraint:trailConstraint];
[self.view addConstraint:heightConstraint];

run一下

效果符合我们的预期。

上面的代码要特别注意的就是translatesAutoresizingMaskIntoConstraints == NO,一定要这样设置,否则会于autoSize冲突导致约束失败。
另一方面我们也看到用这种方式写约束,代码量会瞬间加大不少。于是,聪明的苹果开发人员发明了一种新的语言——Visual format language(VFL)。严格意义上讲这可能算不上是语言,个人感觉更加像是正则表达式一般。

VFL写约束

我们在NSLayoutConstraint类里面还可以看到一个API:

/* Create an array of constraints using an ASCII art-like visual format string.*/
+ (NSArray *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(NSDictionary *)metrics views:(NSDictionary *)views;

这个API支持VFL,返回一组约束(注意是一组约束)。简单解释参数:

format:VFL字符串
opts:约束的一些格式 (目前我还没有具体用到实战中)
metrics:这个字典是自定义的,key可以写在format中,编译器解析时,自动替换为metrics字典中的value。
views:需要约束的所有view.

再次实战,我们将上面的需求用VFL实现:

//绑定需要约束的view 这里只有一个
NSDictionary *bindingDict = NSDictionaryOfVariableBindings(codeView);

//水平方向
NSString *HorizitalVFL = @"H:|-30-[codeView]-50-|";
NSArray *HorizitalArr = [NSLayoutConstraint constraintsWithVisualFormat:HorizitalVFL options:0 metrics:nil views:bindingDict];
[self.view addConstraints:HorizitalArr];

//垂直方向
NSString *VerticalVFL = @"V:|-30-[codeView(==40)]";
NSArray *VerticalArr = [NSLayoutConstraint constraintsWithVisualFormat:VerticalVFL options:0 metrics:nil views:bindingDict];
[self.view addConstraints:VerticalArr];

VFL的语法可以详细见官方文档

解释一下上面的代码:

1.NSDictionary *bindingDict = NSDictionaryOfVariableBindings(codeView)

绑定需要约束的view,可以是一个也可以是多个

2.@"H:|-30-[codeView]-50-|"

H:水平方向,|:superView,-:就是一种符号,可以想象成IB中的那些线条,-30-:30个像素,[]:中括号中的codeView是具体要进行约束的view对象。

所以上面的意思就是:水平方向上,codeView的左侧距离superView30像素,codeView的右侧距离superView50像素

3.@"V:|-30-[codeView(==40)]"

V:垂直方向,[codeView(==40)]中的(==40)说明codeView自身高度约束为40不变

所以上面的意思是:垂直方向上,codeView的顶部距离superView30像素,codeView本身的高度是40像素

4.[NSLayoutConstraint constraintsWithVisualFormat: options: metrics: views:]

用这个方法构造constraits数组。

可以看到用VFL写的约束要比constraintWithItem写的代码量要少很多,而且如果熟悉了VFL之后,理解起来会更加方便。
除此之外,github上还有Masonry这个库,是对Constraint的封装,有兴趣的同学可以去看看。个人认为还是用VFL比较好。

简单总结

通过上面的demo,我相信你可能初步进入了Constraint的世界。现在简单的总结一下:

1.忘记Frame, 是时候和Frame说拜拜了

2.每一个维度(垂直或者水平)上只少有两个约束

3.尽量使用IB而不是用代码来自动布局,除非一些特殊情况例如动画等。因为IB所见即所得,而且它会随时纠正你在布局上的错误

4.记住在手写约束一定要让需要约束的viewtranslatesAutoresizingMaskIntoConstraints == NO

5.property的customView,在用VFL时候要用[_customView]进行约束,而不能用[self.customView]

本文的demo工程已上传github。


本篇拙作较为基础,有不对的地方欢迎指正,接下来会写一篇在实际项目中的autoLayout遇到的一些坑和动画问题。