##背景git
网上有不少使用Storyboard完成UIScrollview
的例子,可是纯代码的例子却很少。有限的一些例子大多也是外国开发者用VFL写的。而这篇文章基于swift语言和SnapKit分析了如何用纯代码加Autolayout写UIScrollview
,完整代码已经上传到个人github。github
在正文中,我会分析其中的关键代码。对于Autolayout,绝对不可取的态度是不停的试几个约束,一旦发现好用,也无论其原理,就放手无论了。事实上,咱们写的每个约束,都要明白它存在的价值是什么,要作到不写一个无用的约束,不漏一个必要的约束,明白为何某种写法有效,而另外一种写法就无效。swift
废话很少说,估计你们用UIScrollView
时,都有过被Autolayout坑的经历,要么是布局不对,要么不能滑动,以及其余匪夷所思的bug。这与Autolayout和UIScrollView
各自的特性有关。布局
首先,咱们知道Autolayout改变了传统的以frame为主的布局思想。它实际上是一种相对布局,核心思想是视图与视图之间的位置关系。好比,咱们能够根据矩形的起始横坐标、纵坐标、长和宽这四个变量肯定它的位置。或者,若是已经肯定矩形A的位置,只要知道矩形B每条边的和A对应边之间的距离,也能肯定B的位置。前者就是frame的思想,它基于绝对数值,然后者是Autolayout的思想,它基于偏移量的概念。spa
其次,UIScrollView
有本身的frame也就是咱们在屏幕上能看到的区域。它还有一个contentSize
的概念。在使用frame布局的时候,咱们通常先设置好子视图的位置,最后再设置contentSize
,它会将全部的子视图包含在内。因而经过滑动,咱们就能够在有限的布局中,看到全部的内容了。code
可是在Autolayout时代,为了简化布局,咱们但愿contentSize
可以自动设置。好比有一个scrollView,它有两个子视图。frame分别为(x: 0, y: 0, width: 10, height: 10)和(x: 10, y: 0, width: 10, height: 10),那么咱们天然会认为这两个视图左右并排排列,contentSize
为(x: 0, y: 0, width: 20, height: 10):对象
这种把若干个子视图合并,得出contentSize
的能力,人类是天生具有的,可是计算机却不是这样。仅凭以上信息,程序没法推断出真正的contentSize
。缘由在于,咱们没有明确的告诉系统,在这两个子视图拼接而成的区域之外,还有没有区域应该被contentSize
包含。ip
也就是说,contentSize
也有多是下图中的阴影部分:开发
若是须要指定contentSize
就是两个正方形拼接而成的区域,咱们还须要提供四个信息:get
……
经过以上的分析,咱们能够看到,其实contentSize
是依赖于子视图自身的大小,和上下左右四个方向的留白大小计算出的。而UIScrollView
的leading/trailing/top/bottom是相对于它的contentSize
而不是bounds
来肯定的。因此若是你写这样的代码,布局是确定不会生效的:
subview.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(scrollView).offset(5)
}
复制代码
由于咱们实际上是在根据UIScrollView
的leading/trailing/top/bottom来肯定子视图的位置,而咱们已经分析过,UIScrollView
的leading/trailing/top/bottom是相对于本身的contentSize
而言的。而contentSize
又是根据子视图位置决定的。这就变成了一种你依赖我,我又依赖你的状况。
为了打破这种循环依赖,为子视图添加约束的两个要求是:
第二个要求意思是说,正常使用autolayout时,咱们肯定一个矩形在水平方向上的范围,只要知道它的左边距离它左边的矩形有多远,以及它有多宽便可。可是在UIScrollView
中布局时,还须要告诉UIScrollView
,它的右边距离右边的视图有多远。这样contentSize
才能肯定。不然UIScrollView
就不知道contentSize
向右能够延伸多少。在竖直方向上也是同理。
**这两大要求必定要牢记!**接下来咱们的代码都将围绕如何知足这两大要求展开。
明白了问题的理论背景后,咱们经过一个具体的需求,来看看正确的代码怎么写,如下面这个效果为例:
如图所示,中间是一个UIScrollView
,它的背景颜色是黄色。红色部分咱们称之为box
,它是一个普通的,红色背景的UIView
。也就是说咱们向UIScrollView
中添加了多个box
,每一个子box
之间间隔必定距离。咱们分步实现这个功能
首先咱们介绍一种使用Container的方法。
###第一步:为scrollView添加约束
let scrollView = UIScrollView()
view.addSubview(scrollView)
scrollView.snp_makeConstraints { (make) -> Void in
make.centerY.equalTo(view.snp_centerY)
make.left.right.equalTo(view)
make.height.equalTo(topScrollHeight)
}
复制代码
咱们以前说过,使用Autolayout时,不用考虑frame布局。因此直接建立一个scrollView
对象。须要先把scrollView
添加到父视图上才能添加约束。
对scrollView
添加约束没有什么难点,就像咱们给其余视图添加约束同样。这里表示scrollView
和父视图左右对齐,居中显示。
###第二步:为container添加约束
scrollView.addSubview(containerView)
containerView.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(scrollView)
make.height.equalTo(topScrollHeight)
}
复制代码
这里对container
的约束很是重要,第一个约束表示本身上、下、左、右和contentSize
的距离为0,所以只要container
的大小肯定,contentSize
也就能够肯定了,由于此时它和container
大小、位置彻底相同。
第二个约束直接经过一个数值,肯定container
的高度。避免了依赖scrollview
布局。这样一来,scrollview
就变成水平的了。container
的宽度直接决定了scrollview
的宽度。
for i in 0...5 {
let box = UIView()
containerView.addSubview(box)
box.snp_makeConstraints(closure: { (make) -> Void in
make.top.height.equalTo(containerView) // 肯定top和height以后,box在竖直方向上彻底肯定
make.width.equalTo(boxWidth) //肯定width后,只要再肯定left,就能够在水平方向上彻底肯定
if i == 0 {
make.left.equalTo(containerView).offset(boxGap / 2) //第一个box的left单独处理
}
else if let previousBox = containerView.subviews[i - 1] as? UIView{
make.left.equalTo(previousBox.snp_right).offset(boxGap) // 在前一个box右侧15个距离
}
if i == 5 {
containerView.snp_makeConstraints(closure: { (make) -> Void in
make.right.equalTo(box) // 肯定container的右侧边界。
})
}
})
}
复制代码
对box
的约束看似复杂,其实很是简单。由于scrollview
在Autolayout下的布局,难点就在于子视图布局时约束比较多。但如今,咱们经过一个container
已经隔离了,也就说咱们又回归了常规的Autolayout布局。以水平方向为例,咱们只要肯定left
和width
便可。
在最后一个if
语句中,咱们为container
添加了右侧的约束。这样就肯定了container
的宽度。因为container
封装了全部的box
,因此对于scrollview
来讲,它的子视图只有一个,就是container
,而container
自身的大小,上下左右四个方向和contentSize
距离在以前的约束中已经被定义为0,contentSize
也就能够肯定了。
##使用外部视图
除了使用container
之外,咱们还可使用外部的视图肯定子视图的位置。这种方法,步骤较少,和以前同样,第一步是建立scrollView
并添加约束。接下来咱们直接添加子视图:
box.snp_makeConstraints(closure: { (make) -> Void in
make.top.equalTo(0)
make.bottom.equalTo(view).offset(-(ScreenHeight - topScrollHeight) / 2) // This bottom can be incorret when device is rotated
make.height.equalTo(topScrollHeight)
make.width.equalTo(boxWidth)
if i == 0 {
make.left.equalTo(boxGap / 2)
}
else if let previousBox = scrollView.subviews[i - 1] as? UIView{
make.left.equalTo(previousBox.snp_right).offset(boxGap)
}
if i == 5 {
make.right.equalTo(scrollView)
}
})
复制代码
这时候,box
是直接add到scrollView
上的。咱们直接指定它的top
为0。前三个约束分别指定了box
的顶部、底部和高度。这样就在竖直方向上知足了两大要求中的第二个要求。对于bottom
的约束,它的参考物是view
,这就是所谓的外部视图。
接下来咱们分别为width
和left
添加了约束。并且只要对最后一个box
添加right
约束便可在水平方向上知足第二个要求。因为咱们的布局依赖于外部的视图,因此天然知足第一个要求,所以这种写法也是能够的。
与container
相比,使用外部视图出了代码量可能略少之外,我实在想不到它还有什么优势。
首先,一旦咱们使用了container
,首先它自然知足第一个要求,由于它并无进行布局,只是让contentSize
与本身等大,而后设置本身的大小。并且它几乎已经知足了第二个要求。只要咱们最后肯定它的宽度或高度便可。其次,在container
内部,子视图布局不用考虑知足第二个要求,由于container
已经隔离了这一切,咱们要作的只是按照习惯,肯定子视图的位置,这样container
的位置也会随着子视图肯定。
其次,我发现的使用外部视图布局的缺点就至少有三个:
left
属性的约束,若是你的代码是这样的:make.left.equalTo(view).offset(boxGap / 2)
复制代码
它和原来的写法几乎是等价的。但你仔细分析,或者试着滑动scrollView
时,必定会大吃一惊。若是你不能一眼看出来这种写法的问题所在,那我建议你运行代码体验一下,而且之后尽可能避免这种写法。