iOS程序员

开始使用AutoLayout

最近,公司iOS团队针对AutoLayout展开了一次探讨,各个业务线的人员也都基本参加了。对于AutoLayout一开始我是拒绝的,因为,你不能说自动布局,我就相信你能自动布局,我要试一下,结果Duang的一下,果不其然的掉到坑里了。

所以,这篇文章里,主要记录下AutoLayout的一些比较有用的工具和一些问题的解决方法。

概念的转变

在开始使用AutoLayout时,很重要的一点就是我们的概念上需要进行一些小的转变,不过这些转变还是很容易去理解的。其实最核心的思想就是,依赖一些固定的元素通过约束来决定不确定元素的大小和坐标。那么哪些是固定的元素呢?比如屏幕的边距,固定大小的元素等,不确定的元素也很多,比如多行文本的高度,不同设备的屏幕宽高等。那么,我们可以通过以下一些原则来加以实践:

  1. UI元素的坐标,可以通过相对距离和对齐方式来确定。
  2. UI元素的大小,可以通过相对距离、约束固定大小、内容大小(intrinsicContentSize)来确定。
  3. 纵横比(aspect ratio),既可以决定坐标,也可以决定大小。

自动布局的所有约束,大致也就分为四类:

  1. 相对距离约束
  2. 对齐方式约束
  3. 大小约束
  4. 分布比例约束

通过这一系列的约束,我们应该能够完整的脑补出一个UI元素在运行时所处的位置和大小,这是需要实际经验来累积的。而在进行布局设计时,我们应该要确保约束的完整性、确定性,自动布局还是相当智能的,虽然没有传统流式布局上直观,但似乎显得更加神秘,并具有魔性。

如何应对UIScrollView

当自动布局遇上UIScrollView,美好的事情发生了,如果你是一个自动布局的新手,那么你会发现怎么约束都不对,这便是自动布局的魔性。

其实只要明白了一点就可以了,苹果在针对UIScrollView自动布局时,进行了特别的处理,也就是UIScrollView的内部元素,相对于UIScrollView的边距约束,只能确定滚动内容的边距,而不能决定它的大小(相当于contentInset),也就是说contentSize是必须要有一个确定大小的内部元素来决定,那么怎样让一个内部元素确定大小,便是解决问题的核心了。

确定内部元素的大小,还是很简单的,可以通过上面提到的几种约束来完成,不过,为了简化UIScrollView自动布局的复杂性,我们一般会在其内部套入一个UIView作为ContentView,这个视图根据设计的需求,我们可以对它加上对应的约束,以此来确定contentSize,比如我们需要一个只能垂直滚动的效果,那么我们的ContentView的宽度约束就不能大于UIScrollView的宽度,而高度约束,应该由内容自上而下的决定,就好比下图:

其中内容的高度是通过内部三个元素来确定的,而宽度是根据外部约束来确定的(比如和UIScrollView宽度相等),ContentView的内部元素,则可以根据ContentView的宽度来进行约束了,因为它的宽度已经被确定了,所以ContentView的内部元素,可以通过左右相对距离,来确定自身宽度。

其它的效果实现方式的核心也会和此类似,所以在使用自动布局时,遇到UIScrollView需要特别注意它内部大小确定的问题,这样你就不会像我一样被坑了。

如何处理UITableViewCell的高度

有了自动布局,确定UITableView单元格高度,似乎有了很方便的处理方式。网上示例很多,这里就不做搬运工了,可以参考以下几篇文章:

如何进行换行

自动布局和流式布局很大一点的差距就是对换行的支持,流式布局可以很容易的实现换行(HTML,Android Activity,WPF等),而通过自动布局,基本上没法自动实现内部元素的折行,还是需要通过计算宽度来手动换行,当然,还可以使用UICollectionView,但又觉得这样的实现太重,目前也没能找到一个特别适合自动布局的换行方式,通过计算来实现换行可以参考这篇文章

使用代码还是Nib

一直以来,使用代码还是nib文件进行布局都倍受争议,而我在其它平台似乎从来没有见到过这样的争议,那么问题到底出在哪里呢?首先我们罗列下各自的优缺点:

  • nib布局的优点
    • 代码量少
    • 修改比较方便,几乎是所见即所得
  • nib布局的缺点
    • 复杂界面在源码合并冲突时非常繁琐
    • 面对一些动态变化的界面无能为力
  • 代码布局的优点
    • 可配置性强,nib能达到的效果都可以达到
    • 源代码冲突合并比较方便
    • 可通过一些策略优化UI性能
  • 代码布局的缺点
    • 代码量大,重复而没乐趣
    • 细节修改比较麻烦,需要不断的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中在什么时机添加约束,也是一个受到了不少争议的地方。既然有争议,那大部分的原因就是苹果设计时没有考虑到这一块,所以给使用者造成了困惑。目前讨论下来,大致有两种方式来添加约束:

  1. 写在updateViewConstraints,在loadview方法最后调用[self.view setNeedsUpdateConstraints],并且加开关控制,避免重复加,类似StackOverflow中这个提问中的回答。
  2. 写在viewDidload里,这篇文章的最后,默认指出最好写在viewDidload里。

我个人觉得这两种方式都无可厚非,也都不是很完美的解决方式,最好是苹果能多推出一个生命周期方法,比如prepareViewConstraints

好用的第三方库

最后要推荐一下,自动布局的两个第三方库:

1. PureLayout

使用代码如下:

- (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中,并添加了开关控制。

2. Masonry

使用代码如下:

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);
}];