Chapter1 Views
View管理指的就是管理view的层次.
而view的层次为其他许多功能提供支持, 如responder链.
View可以从nib加载, 也可以直接通过代码编写, 代码或nib的使用完全取决于你的程序总体结构.
1 The Window
View hierarchy(View树)顶部是Window, 它是UIWindow或UIWindow的子类对象, 而UIWindow也是UIView的子类.
应用中只能有一个main window(主窗口), 主窗口在程序加载时候创建, 并且永远不会被替换或清除.
主窗口是应用中所有可见内容的容器, 即它们的superView.
由此可知, 主窗口是View树的根对象.
NOTE: 如果程序可以显示到额外的屏幕上, 那么会创建另外的UIWindow对象来管理其他view. 其他情况下, 则只有一个screen, 即一个window.
当程序加载时, window会自动覆盖全部screen的尺寸, 即window的frame等于screen的bounds.
如果使用了storyboard, 则这些工作在程序启动时自动完成, 如果没使用storyboard, 则在APP生命期事件方法中写如下代码:
window = UIWindow()
//iOS9之后可以自动将screen的bounds赋值给window的frame.否则就要写下面这句话.
//window?.frame = UIScreen.mainScreen().bounds
为了让window在app生命期中都不消失, 则在AppDelegate对象内增加了一个持有window的属性, 该属性是强引用. 因为程序启动时, 创建了sharedAPP对象, 创建了AppDelegate对象, 而这些对象是被UIApplicationMain过程拥有的, 所以不会被释放, 再由AppDelegate对象持有window, 则window在程序运行时也不会被释放.
一般不要手动在主窗口中添加视图, 而是先实例化出主窗口的根控制器, 然后由根控制器的主视图来显示到主窗口上. 如果使用了storyboard, 则这些工作都自动完成了. 因为window的根控制器就是你的storyboard中初始视图控制器.
一旦window拥有根控制器, 根控制器的主视图就自动成为主窗口在视图树上的唯一子孙, 即成为主窗口的根视图.
APP的主窗口获取有两种方式:
上述的代码创建, 没有storyboard时使用
有storyboard时, UIApplicationMain函数先询问AppDelegate对象, 如果没有window, 则帮它实例化出来一个UIWindow类型的主窗口.类似如下代码理解:
lazy var window : UIWindow? = { return MyWindow() }()
当程序运行时, 有多重手段可以访问主窗口:
当主视图显示到界面上之后(并非加载完成之后), 可以通过它的window属性来获取window. 若它还没有被嵌入到window中, 则window属性为nil.
通过代理对象的window属性访问主窗口( 留意下面代码如何访问代理对象的).
let w = UIApplication.sharedApplication().delegate?.window //或更加特殊的方式 let win = (UIApplication.sharedApplication().delegate as? AppDelegate)?.window!
sharedAPP对象有一个keywindow属性:
let w = UIApplication.sharedApplication().keyWindow
2 Experimenting With Views
见代码, 就是手动加载和使用storyboard的练习…
3 Subview and SuperView
在以前的时候, view都拥有自己的确定的矩形区域, 如果某个view不是另外一个view的subview, 那么就不能在另外这个View的内部显示, 原因是当view绘制的时候, 会重绘它的矩形区域, 这样会让它覆盖掉之前在那个地方的任何view. 即它的subview不会在它矩形框外面显示出来, 因为这个view只管理它自己的矩形框.
但现在, 这样的限制得到很大放松. 现在视图绘制是依据视图树的结构进行的. 即在iOS里, 现在子视图可以部分或全部在父视图矩形框外显示出来, 而且一个视图可以覆盖另外一个视图, 能够部分或全部绘制在非它的父视图前面.
一个view拥有superview属性和subviews属性, 分别表示它的父视图和子视图数组. 并且可以用DescendantOfView:
方法来查询一个view是否是另外一个视图的在某层级上的子视图.
如果需要管理view, 首先想到的是通过outlet属性.
还有一个办法就是使用tag, 为特定view设置唯一tag. 然后向层次中任意父视图发送viewWithTag
方法来获取特定tag的view.
在代码中调整视图树很轻松, 可以将一个view调整到另外一个view的子视图, 则该view的子视图一起过去了就:
- addSubview:
- removeFromSuperview:
记住当从父视图移除子视图时, 便释放了子视图. 当需要再次显示时, 要先获取子视图. 这里所说的释放和获取对应的是ARC内的Release和Retain.
对应的添加子视图或移除子视图事件方法都有:
- didAddSubview:, willRemoveSubview
- didMoveToSuperview:, willMoveToSuperview:
- didMoveToWindow, willMoveToWindow
当调用addSubview方法时, 子视图是被加入到subviews数组的最后一位, 所以被最后绘制, 故也是显示到最前面.
还有一些方法用于将子视图添加到视图层次特定位置, 即对应subviews数组特定下标位置:
insertSubview:atIndex:
在特定位置插入子视图insertSubview:belowSubview:
,insertSubview:aboveSubview:
在特定子视图前后插入子视图exchangeSubviewAtIndex:withSubviewAtIndex:
交换特定两个子视图位置bringSubviewToFront:
,sendSubviewToBack:
将特定子视图移动到最前和最后.
但不爽的是, 没有提供将全部子视图一次性移除的方法.
某视图的subviews数组属性是一个内部subviews列表的不可变拷贝, 故可以通过下面方法移除子视图:
view.subviews.forEach({$0.removeFromSuperview()})
代码中使用forEach方法, 它参数是一个块, 这个块的第一个参数代表的就是每个要遍历的东西, 这里就是view, 然后调用removeFromSuperView即可.
4 可见性和透明度(Visibility and Opacity)
可以设置一个view的hidden属性来显示和隐藏view. 隐藏它并不会将它从视图树中去掉, 而是隐藏显示, 这样可以降低移除它的开销. 隐藏的视图不会接收触摸事件, 但仍然可以在代码中操纵它.
view可以设置背景色, 通过backgroundColor属性. 背景色nil表示这个view有透明背景, 而透明背景的view适合作为其他view的父视图.
可以设置alpha属性来设置view的透明度, 1.0表示不透明, 0.0表示完全透明. 在0-1之间任意取值. view的alpha属性会影响到它的子视图: 如果父视图的alpha是0.5, 则所有子视图的alpha都不会超过0.5, 因为子视图是以alpha值0.5为标准继续绘制的.
颜色也具有alpha值, 即你的view的alpha值为1.0, 但背景色的alpha值为0, 则view一样是透明的.
完全透明的view和隐藏view的行为一致, 也是不会接收触摸事件, 且子视图也全部透明.
view的alpha属性会同时影响到它背景的透明度和子视图的透明度.
view的opaque (不透明, bool类型) 属性则是另外一回事了, 设置该属性不会对view外观有任何影响, 这个属性是给绘图系统的提示, 如果一个视图中以完全不透明的材料填充满它的bounds, 并且它的alpha是1.0, 那么将它设置为true, 将上述事实通知绘图系统, 那么在绘制时会更高效. 否则应该设置为false. 而默认情况是true的.
5 Frame
view的frame属性是一个CGRect结构体类型, 表示视图在父视图中的位置和尺寸, 属性中的坐标系原点定为父视图左上角.
将view的frame赋予新值会改变该view的显示大小或位置, 如果视图可见的话就会反应到界面中.
当然不可见视图也可以重新设置它的frame. 这个时候frame的作用就是指定下次显示该view时应该在何处绘制(新的superVeiw中).
NOTE: 初学者最易犯的错误是给出一个view并添加到父视图, 却忘记设置它的frame. 因为view的默认frame是CGRectZero. 如果想让新建的视图匹配某个尺寸, 可以使用
sizeToFit
属性.
兄弟视图之间的遮挡关系是在subviews数组越后面的view越在顶部, 而每个视图的子视图之间的遮挡关系要分析兄弟之间的遮挡, 然后分析subviews中的前后关系.
6 Bounds and Center
假设视图中包含一个子视图, 需要子视图在父视图中以每边距父视图10点距来缩放显示, 则可以使用Bounds属性, 利用CGRectInset方法或Swift的CGRect中的insetBy方法来设置.
实际CGRectInset方法是在该视图原有基础上进行缩放, 将原bounds缩放一个dx和dy.
因子视图之前已经有一个frame, 故使用bound属性在它自己本身的坐标系下修改尺寸.
下面是bounds的常用方式:
let v1 = UIView(frame: CGRectMake(113, 111, 132, 194))
v1.backgroundColor = UIColor.redColor()
let v2 = UIView(frame: v1.bounds.insetBy(dx: 10, dy: 10))
v2.backgroundColor = UIColor.blueColor()
view.addSubview(v1)
v1.addSubview(v2)
先创建一个view, 在以它的bounds为参照在其中放置一个缩小的view, 利用的是insetBy方法.
当改变bounds的size时, 也会改变frame的size, 改变长宽是以center为参照的, 即向两边扩展.
v1.bounds.origin.x += 10
v1.bounds.origin.y += 10
若改变父视图的bounds原点, 则子视图会相应移动. 意思是父视图以前的原点为(0, 0), 现在的原点为(10, 10)了.
改变frame的坐标, 是在以父视图左上角为原点的坐标系下移动.
改变父视图的bounds的原点, 就是将以前父视图左上角本来喊成0,0, 现在喊成改变后的值, 子视图为了保持以前的状态.
改变一个视图的位置和尺寸的最佳方式是改变它的center和bounds, 而非frame.
总地来说, frame描述在父视图坐标系中位置, 而bounds是描述本地坐标系的.
可以在同一window下的两个视图之间坐标转换, 使用convertPoint:fromView:, convertPoint:toView:,convertRect:fromeView:, convertRect:toView:.
WARNING: 若通过center设置view的位置, 但此时尺寸并非整数, 则会造成错误对齐, 即位置有偏差.
可以通过Debug->Color Misalligned Images来查看. 解决方法是在放置后, 设置view的frame为CGRectIntergral, 或(在Swift中)frame调用
makeintergralinPlace
方法.
7 窗口坐标和屏幕坐标
设备屏幕即screen, 它不具有frame, 但是有bounds, main window没有父视图, 但是它的frame是和screenbounds关联的. 故代码中直接使用UIWindow()创建的窗口和screen一样大.
大部分情况下, 窗体坐标系即屏幕坐标系. 但iOS9中有些情况不是这样, 这里先不说.
iOS7之前屏幕坐标系是不变的, 不管旋转与否. iOS8之后引入了一项大的变化, 当应用随设备旋转的时候, screen和window也随着一起旋转. 这表明当竖直的时候, W比H小, 水平的时候, W比H大.
如果想获得独立于应用旋转的屏幕坐标系, 可以用screen的两个属性来获得, 这两个属性都是UICoordinateSpace协议类型:
UIScreen的coordinateSpace属性:
这个属性会随着屏幕旋转而变化, 坐标原点一直在应用的左上角.
UIScreen的fixedCoordinateSpace属性:
这个属性是固定不变的, 坐标原点在屏幕正常位置的左上角.
为进行坐标系之间的变换, UICoordinateSpace协议中提供了四种转换方法:
convertPoint:fromCoordinateSpace:
,convertPoint:toCoordinateSpace:
convertRect:fromeCoordinateSpace:
,convertRect:toCoordinateSpace:
假设有一个UIView v在界面内, 想知道v在固定设备坐标系下的坐标, 可以这样做:
let r = v.superview?.convertRect(v.frame, toCoordinateSpace: UIScreen.mainScreen().fixedCoordinateSpace)
但实际中很少需要这样做. 因为所有可视内容都是在根控制器的主视图中开始的, 这样会自动将应用的旋转自适应到设备的旋转, 根控制器中的主视图才是编程时大多数时候都在关心的坐标系.
8 Transform
通过Transform属性改变view的绘制方式.
transform是CGAffineTransform结构体类型, 其中包含变形矩阵内9个值中的6个值(其余3各是常量).
transform中的变换矩阵的赋值, 详见苹果Quartz 2D Programming Guide内的”The Math Behind the Matrices”章节.(就是三种变换及其相互组合, 平移translation, 缩放scale, 旋转rotation)
因为变换方法, 都是提供的对三种基本变换的生成, 比如说生成平移, 生成旋转, 生成缩放.
默认情况下, 视图的变换矩阵是CGAffineTransformIdentity, 即一个单位矩阵, 不会对原始坐标造成任何影响.
任何的Transform都是以视图的center为基准进行的.
变换的一个简单例子:
let v1 = UIView(frame: CGRectMake(113, 111, 132, 194))
v1.backgroundColor = UIColor.redColor()
let v2 = UIView(frame: v1.bounds.insetBy(dx: 10, dy: 10))
v2.backgroundColor = UIColor.yellowColor()
view.addSubview(v1)
v1.addSubview(v2)
// 顺时针旋转45度, 这里的参数是弧度制的, 故转换为弧度
v2.transform = CGAffineTransformMakeRotation(45 * CGFloat(M_PI) / 180.0)
另外还可以进行组合变换:
v2.transform = CGAffineTransformMakeTranslation(100, 0)
//这里的变换没有用到带make字样的方法, 表示是在前者基础上进行变换的
v2.transform = CGAffineTransformRotate(v2.transform, 45 * CGFloat(M_PI) * 180.0)
上面的代码就是先平移再旋转.
反过来也可以先旋转再平移:
v2.transform = CGAffineTransformMakeRotation(45 * CGFloat(M_PI) / 180.0)
v2.transform = CGAffineTransformTranslate(v2.transform, 100, 0)
这里的旋转平移都是基于center点的, 故先平移再旋转和先旋转再平移的图形位置不一样.
还有另外一种办法也可以将两个变换结合起来:
let r = CGAffineTransformMakeRotation(45 * CGFloat(M_PI) / 180.0)
let t = CGAffineTransformMakeTranslation(100, 0)
v2.transform = CGAffineTransformConcat(t, r) //先平移再旋转
利用Concat方法将两个Transform连接起来(其实就是矩阵相乘).
顺序相关的意思其实就是因为矩阵乘法时没有交换律.
这里演示如何进行形状改变:
v2.transform = CGAffineTransformMake(1, 0, -0.2, 1, 0, 0)
这里的6个参数分别对应官文中变换矩阵内的:a, b, c, d, tx, ty.
当进行临时的可视化提示时, 使用Transform非常简便.
在iOS7之前, Transform属性是应用旋转功能的核心, 因为窗体的frame和bounds都是固定的, 如果要让应用随设备旋转, 则必须使用Transform设置旋转.
但iOS8之后就不是这样的了. 屏幕的坐标系会相应旋转, 但坐标系本身没有Transform属性. 所以坐标系旋转是假设的: 这里面所有的view都没有参与旋转.
着眼点应放在window的二维坐标系下, 以及根控制器的主视图上面. 这也许表示的是关注它们的绝对坐标, 但实际上经常表示的是它们的本地坐标系是处在一系列的size classes中的, size classes由视图的traitConllection属性引入, 并且是UITraitCollection类型的.
可以将应用的旋转视作界面部分的旋转: 当应用旋转时, 根视图的, 窗体的, 以及屏幕的bounds对应坐标系中的长轴变为短轴, 而短轴则反过来变为长轴.
9 Trait Collection and Size Classes
界面上从主窗口开始往下, 以及所有的VC, 只要有显示到界面上的视图, 都继承了环境中的traitCollection属性值. traitCollection类型实现了UITraitEnvironment协议.
traitCollection属性是UITraitCollection类型的, 里面包含4个成员:
- displayScale: 从当前屏幕得到的缩放系数, 通常是1或2, 以及3(iPhone 6 Plus).
- userInterfaceIdiom: 类型为UserInterfaceIdiom, 它的值表示设备的类型, .Phone或.Pad.
- horizontalSizeClass 和 verticalSizeClass: 是UIUserInterfaceSizeClass类型的, 取值有两种, .Regular或.Compact. 这就被称为Size Class(尺寸类别), Size Class的组合有如下几种含义:
- 水平和竖直方向都是 .Regular时: 运行在iPad上面.
- 竖直为 .Regular, 水平为 .Compact : 在iPhone上面的竖直旋转显示. 反过来表示在iPad上的水平分屏多任务配置上显示.
- 水平和竖直都是 .Compact : 表示在iPhone(除开iPhone 6Plus)上的水平旋转显示
- 竖直为 .Compact, 水平为 .Regular: 在iPhone6 plus上的水平旋转显示.
一个程序在不同地方运行时的traitCollection属性值不同. 当旋转时, traitCollection属性也会跟着变换.
故有一个内置事件方法traitCollectionDidChange
, 在程序启动及以后的旋转中就会触发(只要改变traitCollection的值时). 该消息在UITraitEnvironment层次中传递, 对于我们来说最主要是传递给控制器或视图的时候, 此时之前的traitCollection值会作为参数传递过来, 而新的值可以通过self.traitCollection获取.
可以手动创建一个traitCollection, 但不能直接手动赋值给对象的traitCollection属性.
TIP: 不能直接给任何对象的traitCollection直接赋值, 但可以对traitCollection调用一个特殊的方法
setOverrideTraitCollection
进行赋值.
10 Layout
我们已经看到, 当父视图bounds原点改变时, 子视图会移动, 但如果父视图bounds中size改变时, 又会怎么样呢?
一般情况下, 改变父视图的bounds中size不会影响到子视图size及center. 但实际工作中, 经常想通过改变父视图bounds中size时子视图可以对应改变大小和位置, 这称为layout.
几种动态改变父视图size的办法:
- 随着APP旋转到新的位置, 长边和短边的值发生变化.
- 不同手机上面运行的APP有不同的缩放系数, 视图尺寸也随之变化
- 一个universal类型的APP可能会在iPhone或iPad上运行, 视图尺寸也随变化
- 从nib加载的视图可能会被重新调整大小以适应它的父视图.
- 一个视图可能会连同它周围视图一起变化
- iOS9 中, iPad用户可能会改变APP的水平长度, 是iPad多任务界面的一部分功能
在上述情况下, 都可能需要进行layout
Layout有三种主要使用方式:
- Manual layout : 每当视图尺寸变化时, 它都会调用layoutSubviews来重新布局子视图. 当然这需要你重写layoutSubviews方法, 然后想做什么就做什么, 但缺点是工作量略大…
- Autoresizing: iOS6之前自动进行layout的方式, 当父视图尺寸改变时, 子视图会根据自身autoreSizingMask中设置的规则进行自动变化.
- Autolayout: iOS6之后引入的新型自动Layout方式, 此方式的核心是视图上面应用的constraints(约束). 约束(NSLayoutConstraint类型)指用于描述视图的尺寸或位置的对象, 这种尺寸和位置是基于视图之间的关系进行描述. 比如间距等. Autolayout是在layoutSubviews中实现的, 并且使用AutoLayout可以不用写一行代码就达到目的.
当然在实际工作中, 根据需要对三个方式进行灵活应用. 手动布局一直都可以用代码实现, 而autoresizing默认打开的, 如果想关闭, 则设置父视图的autoresizesSubviews属性为false, 或某个view上面已经使用autolayout的时候. 使用autolayout的视图和使用autoresizing的视图可以共存.
10.1 Autoresizing
Autoresizing就相当于视图的弹簧和支撑. 弹簧可以伸缩, 支撑则不能. 弹簧和支撑可以在视图外部或内部设置, 水平或竖直方向均可. 内部可以让子视图尺寸重新调整. 而外部设置可以让子视图位置重新调整.
比如:
- 固定子视图到父视图中心, 父视图尺寸改变时, 子视图尺寸也跟着改变. 此时外部设置支撑, 内部设置弹簧.
- 固定子视图到父视图中心, 父视图尺寸改变时, 子视图尺寸不跟着改变. 则此时外部设置弹簧, 内部设置支撑.
- 父视图右下角有一个按钮固定在该处, 则此时在右上角和左下角设置支撑.
- 父视图顶部固定一个文本框, 当父视图变宽时它也同时变宽, 则设置内部竖直支撑和水平内部弹簧.
代码中的弹簧或支撑是通过视图的autoresizingMask属性设置的, 该属性值是bitMask, 所以才可以进行组合. 设置的值是UIViewAutoresizing结构体中定义的, 代表弹簧. 任何没有被设置的值自动是支撑. 默认的是.None, 即表示所有都是支撑. 但实际中不会全部都是支撑, 因为当父视图改变时子视图也要跟着变.
TIP: 在调试时, 如果打印视图到控制台, 可以看到里面设置的autoreSizingMask, 代表的是设置的”弹簧”的列表. 父视图边界分别用LM, RM, TM, BM表示, 而内部的高和宽用H和W表示. 比如 打印
autoresize = LM + TM
,表示该视图到父视图的左边界和上边界距离是可变的.
Autoresizing还是比较有效且简单的, 但有时正因为它太”简单”了, 因为规则描述的唯一基础就是子视图和父视图之间的关系.
10.2 AutoLayout
对每一个视图而言, AutoLayout都是可选加入的技术. 视图中加入AutoLayout有三种方法:
使用代码为视图添加约束.
从nib中加载的视图, 这个nib勾选了”Use AutoLayout”. 那每个从这个nib加载的视图都开启了AutoLayout.
将界面内的视图设置为自定义UIView类型, 然后在它的实现中重写
requiresConstraintBasedLayout
类方法, 设置返回值true, 则该视图开启使用AutoLayout.第三种方式在需要将本来没有开启AutoLayout的视图开启AutoLayout时使用. 通常给视图添加约束代码的位置是在
updateConstraints
方法中. 但是如果该视图没有开启AutoLayout, 则updateConstraints
方法不会被自动调用.
相比autoreSizing, AutoLayout可以在兄弟视图之间使用约束关系, 并且可以针对每个视图单独开启关闭AutoLayout. 但由于AutoLayout是在superview chain的基础上实现的, 如果某视图使用AutoLayout, 则它所有的父视图都自动使用AutoLayout. 如果该视图某父视图正好是根视图(大部分情况都是这样), 则根视图对应的控制器会开始接收与AutoLayout相关的事件.
TIP: 无法关闭nib中部分视图的AutoLayout. 只能是所有视图都使用或都不使用. 如果想分别对待, 则将它们分割到不同的nib中去.
10.2.1 Constraints
AutoLayout中的约束是NSLayoutConstraint类型的对象, 用于描述视图的绝对尺寸, 或视图中某尺寸值和另外视图中某尺寸值之间的关系, 两视图之间的关系可以是任意, 不一定必须是父子或兄弟关系, 唯一要求的是它们必须是同一祖先的后代, 即在同一个视图树中.
NSLayoutConstraint类型中的主要属性:
1 firstItem, firstAttribute, secondItem, secondAttribute:
两个视图及它们对应属性(属性类型是NSLayoutAttribute)都保存在约束中.
如果是描述某视图绝对尺寸的约束, 则secondItem为nil, secondAttribute值为.NotAnAttribute.
其他的NSLayoutAttribute值还有:
- .Top, .Bottom
- .Left, .Right, .Leading, .Trailing
- .Width, .Height
- .CenterX, .CenterY
- .FirstBaseline, .LastBaseline
.FirstBaseline主要应用在多行的label中, 它表示的是label或类似对象顶部向下的某个距离, 而LastBaseline是label底部向上某个距离.
其它的都好理解, 另外就是Leading和Trailing, 这两个作用和left和right相同, 它们用于国际化, 即有些地区是从右往左的阅读习惯时, 使用leading和trailing在他们的系统里面就代表left或right, 比如一般左是leading, 右是trailing, 但有的地区相反.
而在iOS9 中, 这样的情况是整个界面被自动镜像到从右往左地区的习惯中, 但如果想正常工作, 则你需要设置leading和trailing来替代left和right.
2 multiplier, constant:
这两个是数值,被应用到之前的secondAttribute上面, 以确定firstAttribute值.
firstAttribute = secondAttribute multiplier + constant*
multiplier被secondAttribute乘, 然后再加上constant即为firstAttribute.
3 relation
它是NSLayoutRelation类型, 表示上述的firstAttribute和secondAttribute如何关联在一起. 如果是相等, 则是 firstAttribute = secondAttribute * multiplier + constant, 如果是大于, 则等号变大于号, 以此类推.
4 priority
它的值为1-1000, 某标准的行为对应有标准的优先级. 约束可以有不同优先级, 优先级决定不同约束的应用优先次序.
约束是属于视图的, 一个视图可以有多个约束, 视图中有constraints属性, 并且有对应的对象方法:
- addConstraint:, addConstraints:
- removeConstraint:, removeConstraints:
另外, 如何确定约束属于哪个视图, 方法是: 约束牵扯的两个视图, 如果是父子关系, 约束属于父视图, 如果是兄弟关系, 约束属于它们共同的父视图, 如果是绝对约束, 则属于该视图.
从iOS8 之后, 除了给一个视图显式添加约束, 还可以用NSLayoutConstraint的类方法activateConstraints
来激活约束, 该方法接受一个约束数组, 然后将数组中约束自动加入到合适位置, 将以前需要程序员决定视图位置的工作自动完成. 另外有一个类方法deactivateConstraints
, 可以将数组中的约束从对应视图中全部移除.
另外, 约束自己还有activate属性, 表示激活与否.
除priority和constant外, NSLayoutConstraint对象(约束对象)的其他属性都是只读的. 如果想改变某约束的其他属性值, 只有移除它在重新建一个.
WARNING: 当显式使用约束来确定视图的尺寸和位置后, 就不要再设置视图的frame(以及bounds和center), 后面只能是单独使用约束. 否则如果约束设置了又去设置frame, 当调用layoutSubviews时, 视图会跳回到约束设置的位置. 除非你是在layoutSubviews方法中设置frame.
10.2.2 AutoReSizing Constraints
当给一个视图设置约束时, 会自动将另外那个视图也牵扯进来, 这时需要一个途径将之前通过自动尺寸autoresizingmask或frame定义的另外那个视图中的这些规则全部转换为约束.
运行时会自动完成这样的工作: 它将视图的frame和bitmask设置自动转换为约束. 结果是一个隐式约束集合, 类型为NSAutoresizingMaskLayoutConstraint.
只对被关联进去的视图进行转换隐式约束, 添加约束的视图自动就是显式约束.
但这种转换的前提是被转换视图的translateAutoresizingMaskIntoConstraints属性必须设置为true. 这个属性值当视图从代码创建或nib中未勾选使用自动约束时默认为true.
但如果不想自动转换成隐式约束, 而是自己添加约束, 则应先将视图的该属性设置为false.
如果忘记关闭, 特别是代码创建视图时, 就可能造成隐式约束和显式添加的约束共同作用在视图中的情况发生.
10.2.3 代码中创建约束
约束中属性是在创建时设置, 除了priority可以改变, priority默认为1000.
约束属于谁就向谁添加约束. 并且要记住两点重要内容:
- 约束归属一定不要错
- 会被转换隐式约束的视图一定要关闭约束转换.
由于提供的代码创建约束过于繁琐, 一般不是这样用.
10.2.4 Anchor 描述法
在iOS9中, 新增了一种在代码中描述约束的方法, 这样可以更容易地写约束.
新的写法更加简洁, 但也更加奇特.
这种方法的核心是关注视图的anchor属性:
- topAnchor, bottomAnchor
- leftAnchor, rightAnchor, leadingAnchor, trailingAnchor
- centerXAnchor, centerYAnchor
- firstBaselineAnchor, lastBaselineAnchor
anchor的值都是NSLayoutAnchor类型或其子类类型的对象. 故构造约束的方法变为了使用NSLayoutAnchor的实例方法, 有许多种可供选择, 选择的依据是如果约束中需要另外的Anchor, 以及包含的multiplier和constant的值.
虽然方法很多, 但如果实际用了会发现很好用, 简洁好懂便于维护.
这种新方法需要在和activateConstraints结合使用时很方便, 我们不用关心约束将加入哪个视图. 只是新建约束, 然后将约束激活, 系统自动决定约束归属.
//8句话建立8个约束
let constraintArr1 = [
v2.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
v2.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
v2.topAnchor.constraintEqualToAnchor(v1.topAnchor)!,
v2.heightAnchor.constraintEqualToConstant(80)!,
v3.bottomAnchor.constraintEqualToAnchor(v1.bottomAnchor)!,
v3.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
v3.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
v3.heightAnchor.constraintEqualToConstant(80)!
]
NSLayoutConstraint.activateConstraints(constraintArr1)
10.2.5 Visual Format Language(VFL)
还有一种更为简洁的方式创建约束, 即使用VFL.
这种方式还有个优点是一句话可同时定义多个约束, 尤其当安排一系列的视图在水平和竖直方向上的位置时.
比如下面这条简单语句:
V:|[v2(10)]
其中 V:
表示讨论的是竖直方向的约束, 对应的是H:
水平方向上的约束(默认就是H, 如果不写)
视图名称用方括号括起来[v2]
, |
代表父视图.这里就是指竖直方向上, v2贴着父视图, 如果是用括号接在视图名称之后, 表示设置该方向上该视图的长度. 上面即指定竖直方向v2顶部贴近父视图, 且v2在竖直方向长度为10.
为了使用VFL, 需要使用字典来定位每个视图的名称, 这样不易出错.
好处是安排多个视图间关系容易.
虽说语法有些奇怪, 但还是需要有所了解.
并且, 方法中的参数有一些需要知道含义:
- metrics: 是一个值字典, 即在VFL中使用到的值可以在这里定义, 然后在VFL中使用它的Key即可.
- options: 为一个字节掩码.
- 要设置两个相邻视图之间的距离, 使用 [v1]-20-[v2]这样的语法
完整的介绍详见苹果Visual Format Language中的Auto Layout Guide一章.
10.2.6 Constraints as Objects
许多时候不但需要建立约束, 并且需要重用约束, 这就需要约束对象一直被保持.
比如在将来某个时候你需要对界面进行彻底的改变, 插入或删除一些视图, 此时如果有约束集合内保存着以前的约束, 要进行这样的工作就十分容易了, 每个集合中的约束对应着不同的界面配置.
//第一种配置
let constraintArr1 = [
v2.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
v2.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
v2.topAnchor.constraintEqualToAnchor(v1.topAnchor)!,
v2.heightAnchor.constraintEqualToConstant(80)!,
v3.bottomAnchor.constraintEqualToAnchor(v1.bottomAnchor)!,
v3.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
v3.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
v3.heightAnchor.constraintEqualToConstant(80)!
]
//第二种配置
let constraintArr2 = [
v2.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
v2.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
v2.bottomAnchor.constraintEqualToAnchor(v1.topAnchor)!,
v2.heightAnchor.constraintEqualToConstant(50)!,
v3.bottomAnchor.constraintEqualToAnchor(v1.bottomAnchor)!,
v3.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
v3.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
v3.heightAnchor.constraintEqualToConstant(30)!,
]
NSLayoutConstraint.activateConstraints(constraintArr1) //激活某个配置
两种配置的效果不一样, 见下图: