NSLayoutAnchor基础知识

我相信大家一直以来都是在Interface Builder里使用AutoLayout来添加约束,或者使用第三方库Masonry等来布局。确实在iOS9之前,如果不使用Interface Builder的话,用代码添加约束,很晦涩繁琐,好在有类似Masonry这些三方库。不过在iOS9之后,Apple似乎也意识到了这个问题,给我们开发者带来了NSLayoutAnchor这个类,这让我们以前用代码添加AutoLayout约束变得简洁明了很多。我个人甚至觉得使用NSLayoutAnchor来布局已经很方便,已经不想再使用Masonry。最近正好也看了相关Apple文档,所以想把NSLayoutAnchor相关知识整理于此,方便以后的翻阅。

UILayoutGuide

在讲述NSLayoutAnchor之前,我觉得我们有必要弄懂一些其他的概念,譬如UILayoutGuide等,因为这些在使用NSLayoutAnchor的时候都会用的到。

那什么是UILayoutGuide,其实它就是一个虚拟的矩形区域,可以和AutoLayout交互。它就像一个透明的view一样,但是不会被添加进视图层级,也不会拦截消息调用,它只是一个虚拟的矩形区域,为的就是和AutoLayout交互。譬如有时候你有这种需求:3个view排一行,互相之间的间隔相等。那么中间这个间隔就可以用UILayoutGudie来代替。

了解了这个概念后,我们需要了解我们后续会经常用到的view的两个属性:layoutMarginsGuide,safeAreaLayoutGuide,第一个出现于iOS9,后面那个属性出现于iOS11。

layoutMarginsGuide

layout margin:就是一个视图的内容和它四个边界之间的空隙,如下图:layout margin

view的layoutMarginsGuide属性,继承自UILayoutGuide,也是用来和AutoLayout交互的虚拟区域,代表的就是视图中内容和视图边界之间的间距区域。我们用代码编写AutoLayout约束的时候,可以以它作为参考对象设置约束。即便我们使用Interface Builder的时候,也可以选择是否基于margin设置约束,如下图:constrain to margins

当我们使用Interface Builder的时候,我们在storyboard里拖动一个view的的时候,当拖拽到离边界很近的时候,xcode会自动出现几条蓝色的辅助线,这个其实就是layoutMarginsGuide的区域,默认是左右各20,上下都是0,当然我们也可以手动修改layout margin的大小,如下图:modify layout margin

safeAreaLayoutGuide

在iOS11的时候,Apple提出了safe area的概念,因为有了iPhoneX,取消了home键,要为操作留一些空间,正好也把原来navigation bar,status bar,tab bar这些也都包含在里面,所以提出了安全区域的概念,在安全区域内设计你的App,绝对不会被导航栏等遮挡,顺势也推出了safeAreaLayoutGuide,这个view的属性,也是继承与UILayoutGuide,也是用来和AutoLayout交互用的,不过这个属性和上面的layoutMarginsGuide一样,都是只读属性,因为它们默认都已经先规定好了一个虚拟区域,我们可以直接基于它们,设置AutoLayout的约束。safeAreaLayoutGuide

当然,如果有需要,我们可以根据具体情况来改变一个view的安全区域,要使用additionalSafeAreaInsets这个属性,这里就不展开了,一般而言我们无需自定义安全区域,除非我们自己设计了一些样式布局需要我们这么做。

NSLayoutAnchor

NSLayoutAnchor简述

现在我们来了解一下这个类,什么是NSLayoutAnchor,这个类可以通过一系列流畅的API,创建NSLayoutConstraint类型的约束对象,来进行布局约束的设置,而不用直接和NSLayoutConstraint对象打交道。

我们可以使用UIViewNSView或者UILayoutGuide,选择它们的一个anchor属性,然后调用对应的布局API来进行代码设置AutoLayout。这些anchor属性都是和NSLayoutConstraint.Attribute相对应的。但是,UIView的anchor属性里没有代表margin的属性,你会发现NSLayoutConstraint.Attribute里是有的,所以呢,表示margin其实就是用我们上述提到的layoutMarginsGuide来实现的。

我们来看个例子先,1是以前直接使用NSLayoutConstraint创建约束的代码。2是使用NSLayoutAnchor创建约束的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1.Creating constraints using NSLayoutConstraint
NSLayoutConstraint(item: subview,
attribute: .leading,
relatedBy: .equal,
toItem: view,
attribute: .leadingMargin,
multiplier: 1.0,
constant: 0.0).isActive = true

NSLayoutConstraint(item: subview,
attribute: .trailing,
relatedBy: .equal,
toItem: view,
attribute: .trailingMargin,
multiplier: 1.0,
constant: 0.0).isActive = true


// 2.Creating the same constraints using Layout Anchors
let margins = view.layoutMarginsGuide

subview.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
subview.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true

很明显第二种方法,代码更简洁,也更易读,不仅如此,NSLayoutAnchor还会提供类型检查,能够帮助我们减少一些非法约束的创建,后面会讲到。

其实通常来讲,我们不会直接使用NSLayoutAnchor这个类,我们都是使用它的子类来进行操作的,它的三个子类如下:

  • NSLayoutXAxisAnchor:x轴方向的锚点,用来创建水平方向的约束

  • NSLayoutYAxisAnchor:y轴方向的锚点,用来创建垂直方向的约束

  • NSLayoutDimension:尺寸相关的锚点,用来创建尺寸相关的约束

一般而言我们都是直接使用UIView或者UILayoutGuide的anchor属性来布局,所以这些属性都已经提前被归纳为上面3中子类中的一种。

其实UIView和UILayoutGuide它们的锚点属性如下:

y轴方向的锚点属性x轴方向的锚点属性尺寸相关的锚点属性
bottomAnchorleadingAnchorheightAnchor
centerYAnchorleftAnchorwidthAnchor
topAnchortrailingAnchor
firstBaselineAnchor(UIView有这个属性)rightAnchor
lastBaselineAnchor(UIView有这个属性)centerXAnchor

那到底这个Anchor是什么意思,这个词中文意思是“锚点“,譬如bottomAnchor,按照字面意思是底部锚点,其实就是代表这个view的底部边界的位置,因为这是一个NSLayoutYAxisAnchor属性,也就是只代表y轴方向上的位置,在y轴方向上,它就代表了这个view底部的边界,而无需考虑x轴方向位置。同理,centerYAnchor就表示当前view在y轴方向上居中的那个位置锚点,topAnchor就代表了view在y轴方向最上面的边界锚点。x轴方向锚点属性和尺寸相关锚点属性都是类似的。

这里要注意leadingAnchor和leftAnchor,trailingAnchor和rightAnchor。虽然它们有时候效果一样,但还是有很大区别的,leadingAnchor代表view最前面的边界锚点,如果在英文等阅读顺序从左往右的国家,那么leading就代表left,但是在中东阿拉伯语等国家,它们的阅读顺序是从右往左,那么leading就代表了right。然而,leftAnchor无论在哪种环境下,都只是表示在左边,所以尽量使用leadingAnchor和trailingAnchor而不要使用leftAnchor和rightAnchor。

一个view这么多位置锚点属性,差不多将这个view在x轴,y轴,长宽尺寸都已经标注了,标注一个view这么多位置,尺寸的锚点,就是为了和另外一个view对应的位置锚点或尺寸锚点交互,这样才能确定一个view的位置和尺寸。所以,所有的这些锚点属性,都是为了和其他view的锚点属性相交互,通过API产生约束,才能确定位置。并且y轴方向的锚点属性只能和y轴方向的锚点属性交互,x轴方向的锚点属性只能和x轴方向的锚点属性交互,尺寸的锚点属性只能和尺寸的锚点属性交互,这也是NSLayoutAnchor的API可以做类型检查的原因。

NSLayoutAnchor的API用法

继承自NSLayoutAnchor的共有API

1
1. func constraint(equalTo anchor: NSLayoutAnchor<AnchorType>) -> NSLayoutConstraint

这个API表示当前view的锚点属性和传入参数的锚点属性是一致的,最终返回的NSLayoutConstraint类型的约束对象,表示两个view的锚点属性相等一致,并且是相同类型的锚点属性。

看例子,譬如我们设置一个view的左边紧挨着左页边距(margin),那么其实就是需要在x轴方向上,这个view的leadingAnchor和页边距的leadingAnchor在同一个位置,所以让这两个锚点位置重合,也就是相等一致即可:

1
2
3
4
5
6
7
8
9
10
11
12
// 方法一:使用NSLayoutConstraint
NSLayoutConstraint(item: subview,
attribute: .Leading,
relatedBy: .Equal,
toItem: view,
attribute: .LeadingMargin,
multiplier: 1.0,
constant: 0.0).active = true

// 方法二:使用constraintEqualToAnchor:
let margins = view.layoutMarginsGuide
subview.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true

注意的一点是,调用完NSLayoutAnchor的API以后,返回的是一个NSLayoutConstraint类型的约束对象,必须设置它的active属性为true,才是真正让这个约束生效。

1
2
2. func constraint(equalTo anchor: NSLayoutAnchor<AnchorType>, 
constant c: CGFloat) -> NSLayoutConstraint

这个API的意思可以用一个公式表达:调用此方法的view的锚点属性 = 参数view的锚点属性 + 常量值c。

  • 对于NSLayoutXAxisAnchor类型的属性来说,表示在x轴方向上,调用此方法的view位置在参数view的位置后面,距离为c。

  • 对于NSLayoutYAxisAnchor类型的属性来说,表示在Y轴方向上,调用此方法的view位置在参数view的位置下面,距离为c。

  • 对于NSLayoutDimension类型的属性来说,表示调用此方法的view尺寸比参数view的对应尺寸大c。

继续看之前那个例子,现在我们希望subview的左边界不再紧挨着左页边距,而是和左页边距距离8:

1
2
let margins = view.layoutMarginsGuide
subview.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor, constant: 8.0).active = true

这样子,subview在x轴方向上就会里页面的左页边距始终距离8。

NSLayoutXAxisAnchor的API

1
2
1. func constraint(equalToSystemSpacingAfter anchor: NSLayoutXAxisAnchor, 
multiplier: CGFloat) -> NSLayoutConstraint

这个API的意思可以用一个公式表达:调用此方法的view的锚点属性 = 参数view的锚点属性 + 系统默认间距*multiplier。

其实就是在x轴方向上,调用此方法的view的位置在参数view的后面,距离是系统默认间距*multiplier。

1
2. func anchorWithOffset(to otherAnchor: NSLayoutXAxisAnchor) -> NSLayoutDimension

这个API的意思是,在x轴方向上,调用此方法的NSLayoutXAxisAnchor类型锚点和同类型参数锚点之间的间距,并以NSLayoutDimension类型返回。说白了,就是利用两个view在x轴方向上,传入锚点的距离,生成了一个尺寸锚点,这个尺寸锚点可以用来自定义一些布局之类的。

NSLayoutYAxisAnchor的API

1
2
1. func constraint(equalToSystemSpacingBelow anchor: NSLayoutYAxisAnchor, 
multiplier: CGFloat) -> NSLayoutConstraint

这个API的意思可以用一个公式表达:调用此方法的view的锚点属性 = 参数view的锚点属性 + 系统默认间距*multiplier。

其实就是在Y轴方向上,调用此方法的view的位置在参数view的下面,距离是系统默认间距*multiplier。

1
2. func anchorWithOffset(to otherAnchor: NSLayoutYAxisAnchor) -> NSLayoutDimension

和NSLayoutXAxisAnchor的API类似,在Y轴方向上,调用此方法的NSLayoutYAxisAnchor类型锚点和同类型参数锚点之间的间距,并以NSLayoutDimension类型返回。说白了,就是利用两个view在Y轴方向上,传入锚点的距离,生成了一个尺寸锚点,这个尺寸锚点可以用来自定义一些布局之类的。

NsLayoutDimension的API

1
2
1. func constraint(equalTo anchor: NSLayoutDimension, 
multiplier m: CGFloat) -> NSLayoutConstraint

这个API的意思可以用一个公式表达:调用此方法的view的尺寸锚点属性 = 参数view的尺寸锚点属性 * m。

看个例子,比如我们有两个view,一个redview,一个blueview,其中blueview的宽是redview的2倍,这样的布局需求很常见把,看代码:

1
blueview.widthAnchor.constraint(equalTo: redview.widthAnchor, multiplier: 2.0).isActive = true
1
2
3
2. func constraint(equalTo anchor: NSLayoutDimension, 
multiplier m: CGFloat,
constant c: CGFloat) -> NSLayoutConstraint

这个API的意思可以用一个公式表达:调用此方法的view的尺寸锚点属性 = 参数view的尺寸锚点属性 * m + 常量c。

继续看上面的例子,现在我们希望blueview的宽是redview的2倍还要多10,那么代码如下:

1
blueview.widthAnchor.constraint(equalTo: redview.widthAnchor, multiplier: 2.0, constant: 10.0).isActive = true
1
3. func constraint(equalToConstant c: CGFloat) -> NSLayoutConstraint

这个就很简单了,就是调用此方法的view的尺寸锚点属性 = 常量c。就是我们通常需要固定一个view的宽高为常量值,那么就用这个API。譬如我们固定上述例子里,blueview的宽为50,代码如下:

1
blueview.widthAnchor.constraint(equalToConstant: 50.0).isActive = true

动手完整的实现一个代码布局

我们回过头来看一下,为什么Apple会设计NSLayoutAnchor的三个不同子类,其实,确定一个view的位置,恰恰就是确定x轴方向的位置,y轴方向的位置,然后就是宽高尺寸。这三个要素一确定,那么这个视图的布局也就确定了,所以我们在用代码写视图布局的时候,也就是考虑这三个方面,x轴方向位置是否确定了?y轴方向位置是否确定了?宽高尺寸是否确定了?说了这么多,我们来自己动手练一下把。

假设我们现在有这样的布局需求,两个view,一个红色的view,一个绿色的view,这两个view左右排列,撑满整个屏幕,但是离屏幕的边界(不是内容边距margin)都有20的间隙,两个view之间相隔8,并且绿色的view宽度是红色view的两倍。

如果我们直接在Interface Builder里设置约束,可以很快实现,但有时候,无法使用Interface Builder,那么我们就需要使用代码来布局了。

注意一点,如果我们需要对一个view使用代码进行AutoLayout设置,我们需要将它的translatesAutoresizingMaskIntoConstraints属性设为false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
redView.translatesAutoresizingMaskIntoConstraints = false
greenView.translatesAutoresizingMaskIntoConstraints = false

//获取安全区域的layoutGudie
let safeArea = self.view.safeAreaLayoutGuide

//Y轴方向布局
redView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 20.0).isActive = true
safeArea.bottomAnchor.constraint(equalTo:redView.bottomAnchor , constant: 20.0).isActive = true
greenView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 20.0).isActive = true
safeArea.bottomAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 20.0).isActive = true
//X轴方向布局
redView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 20.0).isActive = true
greenView.leadingAnchor.constraint(equalTo: redView.trailingAnchor, constant: 8.0).isActive = true
safeArea.trailingAnchor.constraint(equalTo: greenView.trailingAnchor, constant:20.0).isActive = true
//尺寸相关的布局
greenView.widthAnchor.constraint(equalTo: redView.widthAnchor, multiplier: 2.0).isActive = true

布局效果图

总结

NSLayoutAnchor相关知识就介绍到此,对于我们平常iOS开发而言,能使用Interfac Builder设置约束那就最好了,如果某些情况下需要代码来设置约束,使用AutoLayout,那么NSLayoutAnchor可以能够帮我们很大的忙,比起原来直接创建NSLayoutConstraint对象,要方便且易读很多。以上记录是我的一些学习心得,方便自己和有需要的同学参考,详尽知识还请参考Apple官方文档

Author: Uncle Peter
Link: http://yoursite.com/2018/01/28/NSLayoutAnchor基础知识/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.