最近,公司iOS团队针对AutoLayout展开了一次探讨,各个业务线的人员也都基本参加了。对于AutoLayout一开始我是拒绝的,因为,你不能说自动布局,我就相信你能自动布局,我要试一下,结果Duang的一下,果不其然的掉到坑里了。
所以,这篇文章里,主要记录下AutoLayout的一些比较有用的工具和一些问题的解决方法。
在开始使用AutoLayout时,很重要的一点就是我们的概念上需要进行一些小的转变,不过这些转变还是很容易去理解的。其实最核心的思想就是,依赖一些固定的元素通过约束来决定不确定元素的大小和坐标。那么哪些是固定的元素呢?比如屏幕的边距,固定大小的元素等,不确定的元素也很多,比如多行文本的高度,不同设备的屏幕宽高等。那么,我们可以通过以下一些原则来加以实践:
自动布局的所有约束,大致也就分为四类:
通过这一系列的约束,我们应该能够完整的脑补出一个UI元素在运行时所处的位置和大小,这是需要实际经验来累积的。而在进行布局设计时,我们应该要确保约束的完整性、确定性,自动布局还是相当智能的,虽然没有传统流式布局上直观,但似乎显得更加神秘,并具有魔性。
当自动布局遇上UIScrollView
,美好的事情发生了,如果你是一个自动布局的新手,那么你会发现怎么约束都不对,这便是自动布局的魔性。
其实只要明白了一点就可以了,苹果在针对UIScrollView
自动布局时,进行了特别的处理,也就是UIScrollView
的内部元素,相对于UIScrollView
的边距约束,只能确定滚动内容的边距,而不能决定它的大小(相当于contentInset
),也就是说contentSize
是必须要有一个确定大小的内部元素来决定,那么怎样让一个内部元素确定大小,便是解决问题的核心了。
确定内部元素的大小,还是很简单的,可以通过上面提到的几种约束来完成,不过,为了简化UIScrollView
自动布局的复杂性,我们一般会在其内部套入一个UIView
作为ContentView
,这个视图根据设计的需求,我们可以对它加上对应的约束,以此来确定contentSize
,比如我们需要一个只能垂直滚动的效果,那么我们的ContentView
的宽度约束就不能大于UIScrollView
的宽度,而高度约束,应该由内容自上而下的决定,就好比下图:
其中内容的高度是通过内部三个元素来确定的,而宽度是根据外部约束来确定的(比如和UIScrollView
宽度相等),ContentView
的内部元素,则可以根据ContentView
的宽度来进行约束了,因为它的宽度已经被确定了,所以ContentView
的内部元素,可以通过左右相对距离,来确定自身宽度。
其它的效果实现方式的核心也会和此类似,所以在使用自动布局时,遇到UIScrollView
需要特别注意它内部大小确定的问题,这样你就不会像我一样被坑了。
有了自动布局,确定UITableView
单元格高度,似乎有了很方便的处理方式。网上示例很多,这里就不做搬运工了,可以参考以下几篇文章:
自动布局和流式布局很大一点的差距就是对换行的支持,流式布局可以很容易的实现换行(HTML,Android Activity,WPF等),而通过自动布局,基本上没法自动实现内部元素的折行,还是需要通过计算宽度来手动换行,当然,还可以使用UICollectionView
,但又觉得这样的实现太重,目前也没能找到一个特别适合自动布局的换行方式,通过计算来实现换行可以参考这篇文章。
一直以来,使用代码还是nib文件进行布局都倍受争议,而我在其它平台似乎从来没有见到过这样的争议,那么问题到底出在哪里呢?首先我们罗列下各自的优缺点:
Command+R
为什么一个在应用开发中如此重要的环节,会有这么多让我们需要权衡的地方?我把它归结于苹果的独树一帜,在自动布局出来之前,苹果的布局和微软
WinForm
开发的布局是非常类似的,但有一点不同导致了他们在源码合并的友好性上有了天壤之别,苹果的nib文件最终是编译成可执行代码,这个代码我们是不可控的,唯一可控的是nib文件本身,也就是一个xml文件;而微软的WinForm
在设计时就自动生成了源码,通过修改源码就可以修改对应的布局界面,没有任何多余的中间文件。原始的这种静态布局方式,在面对容器大小变化时是非常不友好的,
HTML
天生就考虑到了这样的因素,所以它默认就是流式布局,也就是从左到右,从上到下,并且可以根据样式来设定HTML
元素坐标相对于容器是固定、相对或是静态。通过盒子模型,加上流式布局,以及元素间坐标定位,可以说HTML
简单并很完美的解决了静态布局的缺陷。于是Google的Android和微软的WPF,从一定的程度上都借鉴了HTML
的布局方式,而这两者,在布局上可以说几乎是没什么缺陷可言,此时的苹果却推出了AutoLayout,一个复杂却不讨好的布局方式。
那么问题来了,我们究竟该什么时候使用代码,什么时候使用nib呢?我觉得,那些动态性不强的界面,可以使用nib来布局,如果这个界面比较复杂,则通过拆分子视图的方式来减少一个设计视图中子视图的数量,这样对源码冲突合并也有好处。另外,为了可读性,我建议在nib布局时,IB中的视图最好都打上有意义的标签,如下图:
那么使用代码布局的时机也是很明了了,就是在觉得使用nib布局不适合的时候,虽然苹果的布局自身有很多槽点,但无论如何,官方首推的还是通过nib来完成一些重复性的编码工作。说不定哪天,IB所做的一个简单的事情,用代码要花上几十倍的时间,那时候再来和nib文件亲和,似乎就会晚了点了。所以,少年郎,赶快去细看nib文件中xml元素代表的语义吧!
在使用代码进行自动布局时,ViewController
中在什么时机添加约束,也是一个受到了不少争议的地方。既然有争议,那大部分的原因就是苹果设计时没有考虑到这一块,所以给使用者造成了困惑。目前讨论下来,大致有两种方式来添加约束:
updateViewConstraints
,在loadview
方法最后调用[self.view setNeedsUpdateConstraints]
,并且加开关控制,避免重复加,类似StackOverflow中这个提问中的回答。viewDidload
里,这篇文章的最后,默认指出最好写在viewDidload
里。我个人觉得这两种方式都无可厚非,也都不是很完美的解决方式,最好是苹果能多推出一个生命周期方法,比如prepareViewConstraints
。
最后要推荐一下,自动布局的两个第三方库:
使用代码如下:
- (void)updateViewConstraints
{
if (!self.didSetupConstraints) {
NSArray *views = @[self.redView, self.blueView, self.yellowView, self.greenView];
// Create the constraints that define the horizontal layout, but don't install any of them - just store them for now
self.horizontalLayoutConstraints = [UIView autoCreateConstraintsWithoutInstalling:^{
[views autoSetViewsDimension:ALDimensionHeight toSize:40.0];
[views autoDistributeViewsAlongAxis:ALAxisHorizontal alignedTo:ALAttributeHorizontal withFixedSpacing:10.0 insetSpacing:YES matchedSizes:YES];
[self.redView autoAlignAxisToSuperviewAxis:ALAxisHorizontal];
}];
// Create the constraints that define the vertical layout, but don't install any of them - just store them for now
self.verticalLayoutConstraints = [UIView autoCreateConstraintsWithoutInstalling:^{
[views autoSetViewsDimension:ALDimensionWidth toSize:60.0];
[views autoDistributeViewsAlongAxis:ALAxisVertical alignedTo:ALAttributeVertical withFixedSpacing:70.0 insetSpacing:YES matchedSizes:YES];
[self.redView autoAlignAxisToSuperviewAxis:ALAxisVertical];
}];
// Start out in the horizontal layout
self.isShowingHorizontalLayout = YES;
[self.horizontalLayoutConstraints autoInstallConstraints];
[self.toggleConstraintsButton autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10.0];
[self.toggleConstraintsButton autoAlignAxisToSuperviewAxis:ALAxisVertical];
self.didSetupConstraints = YES;
}
[super updateViewConstraints];
}
上面Demo的实现,添加约束便是在updateViewConstraints
中,并添加了开关控制。
使用代码如下:
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
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);
}];
或者:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];