史上最簡單的UIScrollView+Autolayout出坑指南

##背景git

網上有不少使用Storyboard完成UIScrollview的例子,可是純代碼的例子卻很少。有限的一些例子大多也是外國開發者用VFL寫的。而這篇文章基於swift語言和SnapKit分析瞭如何用純代碼加Autolayout寫UIScrollview,完整代碼已經上傳到個人githubgithub

在正文中,我會分析其中的關鍵代碼。對於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。緣由在於,咱們沒有明確的告訴系統,在這兩個子視圖拼接而成的區域之外,還有沒有區域應該被contentSize包含。ip

也就是說,contentSize也有多是下圖中的陰影部分:開發

更大的contentSize

若是須要指定contentSize就是兩個正方形拼接而成的區域,咱們還須要提供四個信息:get

  1. 左邊的正方形的左側的邊,距離contentSize左邊的距離爲0
  2. 右邊的正方形的右側的邊,距離contentSize右邊的距離爲0

……

經過以上的分析,咱們能夠看到,其實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又是根據子視圖位置決定的。這就變成了一種你依賴我,我又依賴你的狀況。

爲了打破這種循環依賴,爲子視圖添加約束的兩個要求是:

  1. 它不依賴於任何與scrollview有關佈局,也就是不能參考scrollview的位置和大小。
  2. 它不只要肯定過本身的大小,還要肯定本身與contentSize四周的距離。

第二個要求意思是說,正常使用autolayout時,咱們肯定一個矩形在水平方向上的範圍,只要知道它的左邊距離它左邊的矩形有多遠,以及它有多寬便可。可是在UIScrollView中佈局時,還須要告訴UIScrollView,它的右邊距離右邊的視圖有多遠。這樣contentSize才能肯定。不然UIScrollView就不知道contentSize向右能夠延伸多少。在豎直方向上也是同理。

**這兩大要求必定要牢記!**接下來咱們的代碼都將圍繞如何知足這兩大要求展開。

動手實踐

明白了問題的理論背景後,咱們經過一個具體的需求,來看看正確的代碼怎麼寫,如下面這個效果爲例:

任務目標

如圖所示,中間是一個UIScrollView,它的背景顏色是黃色。紅色部分咱們稱之爲box,它是一個普通的,紅色背景的UIView。也就是說咱們向UIScrollView中添加了多個box,每一個子box之間間隔必定距離。咱們分步實現這個功能

使用container

首先咱們介紹一種使用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的寬度。

第三步:添加box

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佈局。以水平方向爲例,咱們只要肯定leftwidth便可。

在最後一個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,這就是所謂的外部視圖。

接下來咱們分別爲widthleft添加了約束。並且只要對最後一個box添加right約束便可在水平方向上知足第二個要求。因爲咱們的佈局依賴於外部的視圖,因此天然知足第一個要求,所以這種寫法也是能夠的。

Container與外部視圖的優缺點

container相比,使用外部視圖出了代碼量可能略少之外,我實在想不到它還有什麼優勢。

首先,一旦咱們使用了container,首先它自然知足第一個要求,由於它並無進行佈局,只是讓contentSize與本身等大,而後設置本身的大小。並且它幾乎已經知足了第二個要求。只要咱們最後肯定它的寬度或高度便可。其次,在container內部,子視圖佈局不用考慮知足第二個要求,由於container已經隔離了這一切,咱們要作的只是按照習慣,肯定子視圖的位置,這樣container的位置也會隨着子視圖肯定。

其次,我發現的使用外部視圖佈局的缺點就至少有三個:

  1. 它依賴外部視圖進行定位,這樣的寫法不夠優雅
  2. 觀察代碼中對於bottom屬性的約束,它不能完美適配旋轉屏幕後的視圖。由於此時的屏幕長和寬會對調。並且目測沒有什麼好的解決方案。
  3. 佈局過程當中容易踩到坑,好比對於left屬性的約束,若是你的代碼是這樣的:
make.left.equalTo(view).offset(boxGap / 2)
複製代碼

它和原來的寫法幾乎是等價的。但你仔細分析,或者試着滑動scrollView時,必定會大吃一驚。若是你不能一眼看出來這種寫法的問題所在,那我建議你運行代碼體驗一下,而且之後儘可能避免這種寫法。

最後重複一下,代碼地址在github.com/bestswifter…,能夠下載下來把玩研究一番,若是以爲對你有幫助,請給一個star。

相關文章
相關標籤/搜索