iOS Auto Layout学习笔记

Auto Layout 是由苹果公司 UIKit 框架提供的一个用于动态计算 UIView 及其子类的大小和位置的库。

说到 Auto Layout 就不得不说 Cassowary 算法,因为 Auto Layout 是构建在 Cassowary 算法的基础之上的。1997年, Auto Layout 用到的布局算法论文发表,被称为高效的线性方程求解算法。2011年苹果利用 Cassowary 算法为开发者提供了 Auto Layout 自动布局库中。由于 Cassowary 算法的本身的优秀,不仅是苹果公司,许多开发者将其运用到各个不同的开发语言中,如 JavaScript、ASP.NET、Java、C++ 等都有运用 Cassowary 算法的库。从这里也可以看出 Cassowary 算法自身的优秀和先进性,不然不会被运用的如此广泛。

苹果公司在 iOS 6 系统时引入了 Auto Layout ,但是直到现在已经更新到 iOS 12 了,还有很多开发者还是不愿使用 Auto Layout 。主要是对其反人类的语法以及对其性能问题的担忧。

针对 Auto Layout 的一些问题,在 iOS 9 发布时,苹果推出了更简洁语法的NSLayoutAnchor。同时发布了模仿前端Flexbox布局思路的UIStackView,以此为开发者在自动布局上提供更好的选择。

在苹果 WWDC 2018 High Performance Auto Layout 中苹果工程师说: iOS 12将大幅度提升Auto Layout性能,使滑动屏幕时达到满帧。 在 WWDC 2018 What's New in Cocoa Touch 苹果的工程师说了iOS 12对Auto Layout优化后的表现。

从图上可以看出, iOS 11 中视图嵌套的数量的性能快成指数级别增长了,在 iOS 12

中已经基本和手写frame布局的性能类似了。

iOS 6iOS 12 ,苹果也在不断的优化 Auto Layout 的性能,同时为开发者提供更简洁的 API ,如果你还在使用 frame 手写布局,不妨试试 Auto Layout 。下面我将介绍 iOS 中几种常用的布局方法。

2、Auto Layout各个版本不同用法

如我要设置一个宽高为120,居中显示的View,效果如下图:

1、用frame手写布局

UIView *centerView = [[UIView alloc] init];
    centerView.backgroundColor = [UIColor redColor];
    [self.view addSubview:centerView];
    CGFloat width = self.view.frame.size.width;
    CGFloat height = self.view.frame.size.height;
    [centerView setFrame:CGRectMake(width / 2 - (60), height / 2 - (60), 120, 120)];
复制代码

2、iOS 6提供的NSLayoutConstraint语法添加约束

centerView.translatesAutoresizingMaskIntoConstraints = NO;
    NSLayoutConstraint *consW = [NSLayoutConstraint constraintWithItem:centerView
                                                             attribute:NSLayoutAttributeWidth
                                                             relatedBy:NSLayoutRelationEqual
                                                                toItem:self.view
                                                             attribute:NSLayoutAttributeWidth
                                                            multiplier:0
                                                              constant:120.0
                                 ];
    NSLayoutConstraint *consH = [NSLayoutConstraint constraintWithItem:centerView
                                                             attribute:NSLayoutAttributeHeight
                                                             relatedBy:NSLayoutRelationEqual
                                                                toItem:self.view attribute:NSLayoutAttributeHeight
                                                            multiplier:0
                                                              constant:120.0
                                 ];
    NSLayoutConstraint *consX = [NSLayoutConstraint constraintWithItem:centerView
                                                             attribute:NSLayoutAttributeCenterX
                                                             relatedBy:NSLayoutRelationEqual
                                                                toItem:self.view
                                                             attribute:NSLayoutAttributeCenterX
                                                            multiplier:1.0
                                                              constant:0.0
                                 ];
    NSLayoutConstraint *consY = [NSLayoutConstraint constraintWithItem:centerView
                                                             attribute:NSLayoutAttributeCenterY
                                                             relatedBy:NSLayoutRelationEqual
                                                                toItem:self.view
                                                             attribute:NSLayoutAttributeCenterY
                                                            multiplier:1.0
                                                              constant:0.0
                                 ];
    [self.view addConstraints:@[consW,consH,consX,consY]];

复制代码

3、用VFL语法

centerView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[centerView(120)]" options:0 metrics:nil views:views]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[centerView(120)]" options:0 metrics:nil views:views]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:centerView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:centerView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
复制代码

4、使用第三方开源框架 MasonrySnapKit

__weak typeof (self) weakSelf = self;
 [centerView mas_makeConstraints:^(MASConstraintMaker *make) {
     make.size.mas_equalTo(CGSizeMake(120, 120));
     make.center.equalTo(weakSelf.view);
 }];

复制代码
let centerView:UIView = UIView.init()
 view.addSubview(centerView)
 centerView.backgroundColor = UIColor.red
 centerView.snp.makeConstraints { (make) in
    make.width.equalTo(120)
    make.height.equalTo(120)
    make.center.equalTo(view)
 }
复制代码

5、使用iOS 9之后Apple提供的NSLayoutAnchor

let centerView:UIView = UIView.init()
 view.addSubview(centerView)
 centerView.backgroundColor = UIColor.red
 centerView.translatesAutoresizingMaskIntoConstraints = false
 centerView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
 centerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
 centerView.widthAnchor.constraint(equalToConstant: 120).isActive = true
 centerView.heightAnchor.constraint(equalToConstant: 120).isActive = true
复制代码

通过上面的代码对比,使用 frame 手写布局只要几行代码就搞定了,使用 NSLayoutConstraint 语法和 VFL 语法是最复杂的,尤其是 NSLayoutConstraint 语法要用30多行代码才能是想同样的效果,代码行数越多出错的概率也就成正比上升,所以这就是很多开发者不愿使用 Auto Layout (或者说不愿意使用系统提供API来实现)的原因之一吧。

如果你的 App 要兼容 iOS 9 以下的各个版本,建议使用 Masonry ,如果只兼容iOS 9以上的版本,建议使用 SnapKit 或者系统提供的NSLayoutAnchor API,毕竟 Masonry 这个库已经2年没有更新了。

在这里我推荐优先使用 NSLayoutAnchor ,第三方的开源库随时都面临着一些问题:

iOS

3、 Auto Layout 的生命周期

前面说到苹果的 Auto Layout 是基于 Cassowary 算法的,苹果在此基础上提供了一套 Layout Engine 引擎,由它来管理页面的布局,来完成创建、更新、销毁等。

APP 启动后,会开启一个常驻线程来监听约束变化,当约束发生变化后会出发 Deffered Layout Pass (延迟布局传递),在里面做容错处理(如有些视图在更新约束时没有确定或缺失布局申明),完成后进入约束监听变化的状态。

当下一次刷新视图(如调用 layoutIfNeeded() )时, Layout Engine 会从上到下调用 layoutSubviews() ,然后通过 Cassowary 算法计算各个子视图的大小和位置,算出来后将子视图的 framelayout Engine 里拷贝出来,在之后的处理就和手写 frame 的绘制、渲染的过程一样了。使用 Auto Layout 和手写 frame 多的工作就在布局计算上。

4、 NSLayoutAnchor 常用属性

  • leadingAnchor
  • trailingAnchor
  • leftAnchor
  • rightAnchor
  • topAnchor
  • bottomAnchor
  • widthAnchor
  • heightAnchor
  • centerXAnchor
  • centerYAnchor
  • firstBaselineAnchor
  • lastBaselineAnchor

对于 NSLayoutAnchor 的一些常用属性,通过其命名就能看出来其作用,这里不做赘述,如果想了解更多请查阅 Apple Developer NSLayoutAnchor

5、Auto Layout几个更新约束的方法

  • setNeedsLayout: 告知页面需要更新,但是不会立刻开始更新。执行后会立刻调用 layoutSubviews

  • layoutIfNeeded: 告知页面布局立刻更新。所以一般都会和 setNeedsLayout 一起使用。如果希望立刻生成新的 frame 需要调用此方法,利用这点一般布局动画可以在更新布局后直接使用这个方法让动画生效。

  • layoutSubviews: 更新子 View 约束

  • setNeedsUpdateConstraints :需要更新约束,但是不会立刻开始

  • updateConstraintsIfNeeded :立刻更新约束

  • updateConstraints:更新 View 约束

6、 NSLayoutAnchor 使用注意事项

1、在使用 NSLayoutAnchor 为视图添加约束时一定要先把 translatesAutoresizingMaskIntoConstraints 设置 false

centerView.translatesAutoresizingMaskIntoConstraints = false
复制代码

2、在使用 safeAreaLayoutGuide 适配 iPhone X 等机型时要对 iOS 11 之前的系统做适配,否则会导致低版本系统上程序Crash

if #available(iOS 11.0, *) {
     tableView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
 } else {
     tableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
 }
复制代码

3、设置约束后要将其激活,即设置 isActivetrue

let centerX: NSLayoutConstraint = centerView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
centerX.isActive = true
复制代码

4、 leadingAnchor 不要和 leftAnchor 混用

centerView.leadingAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
复制代码
centerView.leftAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
复制代码

以上2种写法,在编译时不会出现任何问题,但是在运行时就会报错,并会导致程序Crash,官方的说法是:

While the NSLayoutAnchor class provides additional type checking, it is still possible to create 
invalid constraints. For example, the compiler allows you to constrain one view’s leadingAnchor
 with another view’s leftAnchor, since they are both NSLayoutXAxisAnchor instances. However, 
Auto Layout does not allow constraints that mix leading and trailing attributes with left or right 
attributes. As a result, this constraint crashes at runtime.
复制代码

同理, trailingAnchorrightAnchor 也不能混用。

5、如何刷新某个约束

如我要修改一个 UIView 的宽度: 通过代码添加约束,可把 UIView 的宽度设置类属性,然后在需要的地方修改 constant 的参数,然后在刷新约束即可,代码如下:

var centerView: UIView! 
 var centerWidth: NSLayoutConstraint! 

复制代码
self.centerView = UIView.init()
view.addSubview(self.centerView)
self.centerView.backgroundColor = UIColor.red
self.centerView.translatesAutoresizingMaskIntoConstraints = false
self.centerView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
self.centerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
self.centerWidth = self.centerView.widthAnchor.constraint(equalToConstant: 120)
self.centerWidth.isActive = true
self.centerView.heightAnchor.constraint(equalToConstant: 120).isActive = true
复制代码
self.centerWidth.constant = 250
weak var weakSelf = self
UIView.animate(withDuration: 0.35, animations: {
   weakSelf?.centerView.superview?.layoutIfNeeded()
}) { (finished) in
            
}
复制代码

效果如下:

如果是 xib 或者 storyboard ,那就更简单了,直接摁住键盘 control 键,拖到对应的类里,然后在需要的地方修改约束并刷新即可。操作如下:

6、设置宽高比

在开发中,我们会遇到一些需求要求根据 UIView 的宽高比来设置约束,如一般情况下显示视频的宽高比是16:9,通过代码设置宽高比如下:

centerView.heightAnchor.constraint(equalToConstant: 90).isActive = true
 centerView.widthAnchor.constraint(equalTo: centerView.heightAnchor, multiplier: 16 / 9).isActive = true
复制代码

7、 Auto Layout 自适应 UITableViewCell 高度使用

1、 使用 rowHeight 设置高度

一般情况下,如果 UITableView 的每个 Cell 高度是固定的我们可以直接指定一个值即可,如果没有设置 UITableView 的高度,系统会默认设置 rowHeight 高度是44。

tableview.rowHeight = 44;
复制代码

也可以通过UITableViewDelegate的代理来设置UItableView的高度。

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 50
 }
复制代码

如果通过手动计算每个 UItableViewCell 的高度,也在这个代理中实现,通过计算返回每个 UItableViewCell 的高度。

2、使用 estimatedRowHeight 设置高度

UItableView 继承自 UIScrollView , UIScrollView 的滚动需要设置其 contentSize 后,然后根据自身的 bounds、contentInset、contentOffset 等属性来计算出可滚动的长度。而 UITableView 在初始化时并不知道这些参数,只有在设置了 delegatedataSource 之后,根据创建的 UITableViewCell 的个数和加载的 UITableViewCell 的高度之后才能算出可滚动的长度。

在使用 Auto Layout 自适应 UITableViewCell 高度时应提前设置一个估算值,当然这个估算值越接近真实值越好。

tableView.rowHeight = UITableView.automaticDimension
 tableView.estimatedRowHeight = 200
复制代码
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return 200    
 }
复制代码

如上图所示:这个界面就是用 Auto Layout + estimatedRowHeight 完成自适应高度的,在添加约束时要按照从上到下的书讯设置每一个 UIView 的顶部( top )到上一个的视图底部的( bottom )距离,同时要计算 UITableViewCell 内部所有控件的高度。那么问题来了,用户发布的内容详情没有得到数据之前时没办法算出其高度的,此处可以先给内容文字 UILabel 设置一个默认高度,然后让其根据内容填充自动计算高度:

topicInfoLab.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true;
 topicInfoLab.font = UIFont.init(name: "Montserrat-SemiBold", size: 12)
topicInfoLab.numberOfLines = 0
复制代码

如果用户发布内容没有图片,直接设置发布内容UILabel距离UITableView距离底部的约束距离即可;

detailsLab.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8).isActive = true

复制代码

如果用户发布的内容有图片,那么在计算出每张图片的位置和大小之后,一定要给最后一张图片设置距离 UItableViewCell 底部( bottom )的约束距离。

for(idx, obj) in imageArray.enumerated() {
//.....计算图片的大小和位置
if idx == imageArray.count - 1 {
   //设置最后一张图片距离底部的约束
   photo.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8).isActive = true
 }
}
复制代码

实现思路如上图所示,具体实现的请看 代码

8、 Compression Resistance PriorityHugging Priority 使用

Compression Resistance PriorityHugging Priority 在实际使用中往往配合使用,分别处理在同义水平线上多个view之间内容过少和内容过多而造成的互相压挤的情况。

Hugging Priority 的意思就是自包裹的优先级,优先级越高,则优先将尺寸按照控件的内容进行填充。

Compression Resistance Priority ,意思是说当不够显示内容时,根据这个优先级进行切割。优先级越低,越容易被切掉。

ContentHuggingPriority 表示当前的 UIView 的内容不想被拉伸
ContentCompressionResistancePriority 表示当前的 UIView 的内容不想被收缩
默认情况下: HuggingPriority = 250 默认情况下: CompressionResistancePriority = 750

如设置2个 UILabel 的拉伸优先级可使用代码:

fristLab.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .horizontal)
secondLab.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 750), for: .horizontal)
复制代码

9、总结

本文主要分享了苹果 Auto Layout 的几种实现方法和注意事项,对于 Auto Layout 在实际开发中的使用是采用纯代码、还是 xib + 代码,还是 storyboard + 代码,还是 xib + storyboard + 代码的方式实现,主要看团队的要求、个人的习惯,以及 App 的繁琐程度。 对于 Auto Layout 在视图上的使用,个人建议如果UI比较简单或者单一的界面可使用 Auto Layout ,如果UI的操作或刷新很复杂的界面,建议还是 frame + 手动布局的方式。

本文demo,请戳这里

友情链接:

深入剖析Auto Layout,分析iOS各版本新增特性

Auto Layout 是怎么进行自动布局的,性能如何?

Apple Developer High Performance Auto Layout

Apple Develope NSLayoutConstraint

WWDC 2018 What's New in Cocoa Touch

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章