##背景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
時,必定會大吃一驚。若是你不能一眼看出來這種寫法的問題所在,那我建議你運行代碼體驗一下,而且之後儘可能避免這種寫法。