理論分析

首先,我們知道Autolayout改變了傳統的以frame為主的佈局思想。它其實是一種相對佈局,核心思想是檢視與檢視之間的位置關係。比如,我們可以根據矩形的起始橫座標、縱座標、長和寬這四個變數確定它的位置。或者,如果已經確定矩形A的位置,只要知道矩形B每條邊的和A對應邊之間的距離,也能確定B的位置。前者就是frame的思想,它基於絕對數值,而後者是Autolayout的思想,它基於偏移量的概念。

其次,UIScrollView有自己的frame也就是我們在螢幕上能看到的區域。它還有一個contentSize的概念。在使用frame佈局的時候,我們一般先設定好子檢視的位置,最後再設定contentSize,它會將所有的子檢視包含在內。於是通過滑動,我們就可以在有限的佈局中,看到所有的內容了。

但是在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包含。

也就是說,contentSize也有可能是下圖中的陰影部分:

更大的contentSize

如果需要指定contentSize就是兩個正方形拼接而成的區域,我們還需要提供四個資訊:

  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的寬度。