一、概述
使用autolayout有一段时间了,Objective-C和swift下、iOS7和iOS8下都在用,
一路遇到了不少的坑,随遇随填,到今天也算是积累了不少经验了,这里总结一下,
通过自己新建的一个Doubi Demo来分享给大家。Doubi Demo我已上传到github上
去了(地址是:https://github.com/lihux/iLihuxAutoLayout),以后每篇文章的demo都
将放在github上,供大家参考。源码的workspace中有两个工程,分别使用OC和swift
实现一个相同的功能,大家在学习autolayout的同时也能够通过两种语言的对比来学习
、体会OC和swift编程的差异,以后的Demo也尽量同时用两种语言实现。
在学习英语的时候,老师会告诉我们,真正能够熟练应用英语的标志是You
naturally not only speak in English but think in English,即要学会用英语去思考,而
不仅仅是会将母语思考后的句子在大脑里翻译之后用英语说出来,能够真正做到这一
点,才算真正的掌握了英语。同样的道理,要真正掌握autolayout,就必须忘掉曾经
的frame,学会用autolayout的语言来思考和实践。具体一点儿来说,在创建UI的时候,
在storyboard或者代码中使用autolayout来构建UI而不是直接修改frame;在实现动画
的时候,也同样的通过修改autolayout来实现动画效果。
一般而言,自动布局(autolayout)的使用分为两个步骤:首先是创建autolayout,
这一步,主要是通过代码和storyboard相结合的方式,通过对UI空间(UIView、
UIbutton)等添加约束(constraint),让UI满足我们的产品的静态设计需求;其次
就是根据用户交互来修改或者增删已有UI上的约束,以产生相应的动画效果。
约束的增删改可以通过storyboard添加和代码添加两种方式,前者的优势就是
能够很直观的迅速添加约束,80%-90%的约束都能够通过这种方式迅捷的完成,实
践证明这种方法效率并不比以前的frame+autoresizingmask的方式低;后者的优点
则是动态、灵活,可以在运行时根据需要新建约束,满足特定环境下的需求,而修
改、删除已有约束,通常也只能通过代码来实现。总体而言,会有10%~20%的场景
会用到代码添加约束。这两种方式是互补的,要想游刃有余的使用自动布局为自己
工作,就必须同时掌握这两种方法。
二.详解
下面通过一个实际的Doubi Demo来展示一个自动布局的一般过程,该场景包括:
1)在storyboard中创建约束;
2)通过修改约束来实现简单的动画;
3)特定的场景必须要通过手动代码添加约束;
Demo分为两个场景,场景一如下图所示:
场景1:主要展示storyboard中添加约束以及通过约束实现动画效果
场景2:必须要使用代码添加约束的场景
1)在storyboard中创建约束
在我之前的博文中已经详细讲过如何在sb中创建约束,这里不再赘述,只简单
的列出我所添加的约束,如下图所示:
这里创建约束需要提及的有两点:其一,我们引入了一个辅助的view,如我
前文中讲过,这也是自动布局中常用的技巧之一。通过一个anchor view的引入,
我们能够巧妙的将相对于全局的布局工作转化为相对于局部(anchor view)的布
局工作,一定程度上降低工作量和布局的复杂度。让它恰好位于整个view的正中心
(水平居中+垂直居中)作为我们的“锚点”,然后所有其他的view都(直接或间接
的)以这个辅助view作为参照物来添加约束,这样就能保证整个UI在横屏或者小屏、
手机上也能有一个很好的展示效果。
横屏效果
其二是,我们对Doubi同时在水平方向上施加了leading和trailing两种互斥约
束,这个主要用于后面动画的实现,为了让两个相互冲突的约束能够正常存在,我
们必须要将其中一个约束的优先级调低(默认优先级是1000,这里我们调低至750,
其实是只要比1000低就好)。
2)通过修改约束实现简单的动画
上面我们讲到对于Doubi我们添加了两个互斥的约束,意在实现点击左右两个
按钮时Doubi能够向左和向右滑动:修改Doubi的约束然后调用UIView的animation
block实现动画。
要想修改Doubi身上的约束(constraint)首先就是要能够获取和修改作用于Doubi
上的约束,我之前的一篇博文讲过一种方法,通过遍历其superview上的constraints
数组通过constraint属性的判断来找到这个约束(和父view之间的约束都只放在父view
的constraints属性里而不是自己的constraints属性中)。其实Doubi Demo使用了一
个更好的方法:通过IBOutlet的方式直接将storyboard中的约束引入到代码里:
使用IBOutlet可以直接将约束从storyboard中直接拖到代码里
这里需要注意的时,我们使用了strong属性修饰印出来的约束,这样会防止在
约束不被使用的时候依旧保持一个强应用而不被释放,强引用在OC是用strong关键
字,而在swift里则不用weak修饰的就表示是strong。
下面是OC版本的动画代码:
- (void)animatedMoveDoubiIsLeft:(BOOL)isLeft { if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1) { //ios8 self.kidLeftCenterConstraint.active = isLeft; self.kidRightCenterConstraint.active = !isLeft; [UIView animateWithDuration:kAnimationDuration animations:^{ [self.view layoutIfNeeded]; }]; } else { //ios7 NSLayoutConstraint *constraintToRemove = !isLeft ? self.kidLeftCenterConstraint : self.kidRightCenterConstraint; NSLayoutConstraint *constriaintToUse = isLeft ? self.kidLeftCenterConstraint : self.kidRightCenterConstraint; [self.douBi.superview removeConstraint:constraintToRemove]; [self.douBi.superview removeConstraint:constriaintToUse]; [self.douBi.superview addConstraint:constriaintToUse]; [UIView animateWithDuration:kAnimationDuration animations:^{ [self.view layoutIfNeeded]; }]; } }
在swift中是酱紫的:
func animatedMoveDoubi(isLeft: Bool) { if NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1 { self.kidLeftCenterConstrait.active = isLeft self.kidRightCenterConstrait.active = !isLeft UIView.animateWithDuration(kAnimationDuration, animations: { () -> Void in self.view.layoutIfNeeded() }) } else { let constraintToRemove = isLeft ? self.kidRightCenterConstrait : self.kidLeftCenterConstrait let constriaintToUse = isLeft ? self.kidLeftCenterConstrait : self.kidRightCenterConstrait self.douBi.superview!.removeConstraint(constraintToRemove) self.douBi.superview!.removeConstraint(constriaintToUse) self.douBi.superview!.addConstraint(constriaintToUse) UIView.animateWithDuration(kAnimationDuration, animations: { () -> Void in self.view.layoutIfNeeded() }) } }
需要说明的是:constraint中的active用来标识当前约束在它所在的view使用自
动布局的时候是否被应用,类似UIButton中的enable属性,但是这个属性只有在最新
的iOS8中才被引入,至此如果APP要支持iOS7的设备,则不能使用该属性,而只能
通过删除相关约束来实现。有些童鞋灵机一动也许会说:对了,我们可以修改那俩
互斥的约束的优先级来实现选取其中一个约束来实现动画嘛。实际情况是:不行!
一旦一个约束被建立起来,你能改变的其实很少:一般也只有consant和active属性,
其他属性一旦添加到view之后便不能修改,否则对程序会有未知的后果。建议使用
autolayout的童鞋好好研读一下NSLayoutConstraint.h文件和其参考文档,了解一下
constraint中有哪些成员变量和方法,都有什么作用。
3)特定的应用场景下使用代码添加约束
在Doubi Demo的场景2中,我们创建了一个tableview,运行起来有三个cell,
用户点击cell上的删除图标删除cell,当cell都背删完了的时候,我们希望tableview
展示一个提示内容为空的view出来,这在一般的需要联网的APP中相当常见:当
断网的时候无法获得数据,给用户展示一个view用于提示无网络。这一般都是通过
设置tableview的background view来实现的,我们把一个uiview设置好提示图标,然后
将其赋值给tableview的backgroundView属性即可,之后在返回cell个数的回调方法
中如果cell个数为0就显示empty view,否则隐藏。
在实现上,我们现在storyboard将这个empty view布局好,通过IBOutlet引入
代码中,在viewDidLoad的方法中将其赋值给tableview的backgroundView属性,
注意,在iOS8上是可以直接将empty view赋值给tableView.backgroundView,empty
view和其superview之间添加约束或者不再添加约束都没问题了,但在iOS7中则不
行:如果直接将empty view 直接赋值过去,不管添不添加约束都会奔溃!苹果真是
坑坑不息啊!解决方法是再创建一个不初始化任何约束和frame信息的空view作为
empty view 的父view,将该view赋值给background view,建立好empty view和这个
container view之间的约束关系即可,代码如下所示:
Objective-C版本:
- (void)customUI { UIView *backgroundView = [UIView new]; [backgroundView addSubview:self.emptyView]; NSDictionary *views = @{@"backgroundView": self.emptyView}; [backgroundView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|[backgroundView]|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:views]]; [backgroundView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[backgroundView]|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:views]]; self.tableView.backgroundView = backgroundView; }
Swift版本:
func customUI() { let backgroundView = UIView() backgroundView.addSubview(self.emptyView) let views = ["backgroundView": self.emptyView] self.emptyView.superview?.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|[backgroundView]|", options: NSLayoutFormatOptions.allZeros, metrics: nil, views: views)) self.emptyView.superview?.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[backgroundView]|", options: NSLayoutFormatOptions.allZeros, metrics: nil, views: views)) self.tableView.backgroundView = backgroundView }
代码里我们使用的苹果的VFL语言实现约束,实际上代码创建约束还可以通过
NSLayoutConstraint的constraintWithItem:relatedBy:toItem:attribute:multipler:constant
方法实现,后者稍显麻烦,但是若果要建立放缩因子不为1(multipler !=1)的约束
时就只能通过这种方法实现,VFL语言无法修改放缩因子,另外使用VFL方法创建
出来的是一个约束数组,而后者则创建了一个单独的约束对象,关于这些细节,这
个大家心里要有数。VFL语言格式虽然咋一看上去有些诡异,但其实比较形象、简
单的,如果你认真弄明白了,用起来是相当爽的,分分钟添加一个约束不是什么问
题。研习VFL语言我这里给大家推荐一篇老外写的博文(不过好像被墙了,好吧,
抽空我给翻译过来,除了看苹果官方文档,这一遍文章足矣。):
http://commandshift.co.uk/blog/2013/01/31/visual-format-language-for-autolayout/
三、总结
本文主要从比较粗的范围、依托一个小小的Doubi Demo,总结性的介绍了
autolayout的使用和其中的一些技巧和坑坑。细节部分不再详述,随便一搜,网上
一堆,我就不再重复造轮子了。自动布局的使用在使用了storyboard的项目中,80%
以上的工作都是在storyboard中很轻松的完成(倘若你的项目还没有使用storyboard
(故事版),那么好吧,autolayout使用起来就有些费劲了:你会有很大段很大段的
代码要写,即使是用VFL语言来写,其可读性也远远比不上在storyboard中图形化来
的直观方便)。剩下一些必须要代码实现的部分,诸如通过修改约束实现动画以及一
些特殊只能手动添加约束的场景。读别人文百边,比如自己实践,现在就开始写自己
的布局代码吧,唯有实践是学习的最佳途径!
最后的最后,总结并罗列一些autolayout使用的过程中的经验:
1.一定要熟练的使用storyboard,熟练的掌握如何在storyboard中添加约束,因为这
一块儿占据了80%以上的布局量(什么?Storyboard你还不会用?那赶紧学吧,不要跟我说纯代码写的程序才叫NB,现实是,目前的技术成熟度和产品开发效率的需求都要求你快快使用起storyboard来,况且纯代码里写约束实在是一个费时费力还不讨好的事情,百分百的鸡肋);
2.动画的实现,可以借助IBOutlet来方便的修改在sb中创建的约束来实现动画;
3.学会使用VFL语言,它是你代码实现constraint的一个利器!