ScrollView 中使用 AutoLayout

ScrollView 是 UIKit 中很重要的一個組件,TableView 和 UIWebView 等很多涉及到滑動的視圖都有它的影子,在之前使用代碼布局的時代,只要簡單的進行配置,就 OK 了。但是在 AutoLayout 的時代,ScrollView 就存在了很多坑,今天就是分析坑和填坑的。

矛盾從哪兒來

先上地址是一個好習慣:

Github Demo 地址?

可以 git rest 到對應的階段去看約束的設置。

好比下面舉個例子,先在最上面放置了一個 imageView ,然后設置其約束:

// 設置其水平居中
imageView.centerX = superView.center.X
  
// 然后確定其頂部間距
imageView.top = Top Layout Guide.bottom

// 設置其寬度和高度
imageView.height = 155 
imageView.width = 115
  
// 在這里說明下,auto layout 中,對于 label、imageView 等其中有內容的視圖,有一個叫做固有內容的概念。對于通常的 UIView ,如果你想要使其約束達到完整。首先需要確定它的位置,也就是根據約束能夠推斷出 x 和 y 坐標,還得確定其 height 和 width

// 但如果這個視圖是有固定內容的,那么就可以不主動的去設置其 height 或 width,因為 autoLayout 系統能夠通過視圖中的內容推斷出視圖當前的 height 和 width
  
// 比如,Label,我們不去主動設置 label 的高度,autoLayout 可以根據 label 中字體的大小推斷得到其高度。比如 imageView ,autoLayout 可以根據其中 image 的尺寸得到其 height 和 width

// 關于固有內容還有一些壓縮和擴張的優先級,這里就不做介紹了。
給圖片添加約束

然后拖一個 ScrollView 進去,設置 scrollView 的約束:

// 頂部距離圖片 20 
scrollView.top = image.top + 20

// 左邊、右邊以及下邊都是 20 的距離
scrollView.leading = superView.leading + 20
scrollView.trailing + 20 = superView.trailing + 20
scrollView.bottom + 20 = Botton Layout Guide.top

看一下 scrollView 的約束:

scrollView 的約束

這樣子 scrollView 的約束就確定了。build 一把,也沒啥問題。

然后往 scrollView 中拖幾個 label 、button 等進去,按照平時的習慣設置下約束,哎,好像出問題了。不對啊,我平常不都是這么弄的么,難道 scrollView 還不太一樣嗎?

設置 textField 的約束

看到約束變紅了,表示約束當前出現了問題。對于調試約束有經驗的同學,知道這個時候點擊左上角的紅色箭頭,去看一下,然后發現出了個叫 ScrollView content size ambiguity 的問題。具體說的是 scrollView 的 content height 和 content width 有歧義。

ScrollView content size ambiguity

字面理解,就是 scrollView 的 contentSize 布局有歧義。

contentSize 是 scrollView 中的內容尺寸,scrollView 需要知道自己的內容尺寸從而決定該怎么滑動。

將鼠標移動到黑色粗體的說明上,右邊會顯示出一個詳細按鈕,點擊就能看到具體的說明,可以幫助我們快速了解 autoLayout 布局中的問題。

scroll view content size 布局錯誤

點擊之后得到具體信息如下:

the scrollable size of a uiscrollview is computed automatically based upon the constraints of its subviews. in order to fully define the scrollable content width and height , there needs to be constraints touching all edges (leading ,trailing ,top, and bottom) of the scroll view.

to fix ,make sure you can trace a line of continuously connected constraints within the scroll view's subviews from the leading (or top) edge of scroll view to the trailing (or bottom) edge

隨便翻一下,大概意思就是說:scrollView 的滾動區域是根據子視圖的約束來確定的,即根據所有子視圖的約束,確定 scrollView 需要多大的區域才能全部容納這些子視圖,然后將這個區域大小設置為 scrollView 的 content size。這樣 scrollView 就可以安全的滾來滾去了~

為了能夠充分的定義滾動的區域,那么這些約束必須能夠接觸到所有的邊緣(頂部、底部、左邊、右邊)。通俗的講,只有你的約束設置時關聯到了四個邊緣,才能確定 content size 的大小。因為上下左右的邊距都被設置了,整體的寬高也就確定了。

要修復這種問題的話也很簡單,只要確保在 leading 到 trailing 方向(或 Top 到 bottom)上,子視圖之間有一條不間斷的約束線連接到 scrollView 的邊緣。也是通俗的講,即水平方向和垂直方向上,所有子視圖的約束最終都能推導得出這個方向上的長度。舉個例子,在我們剛才的視圖中,scrollView 中從 Top 到 bottom 放置了兩個 textField。在水平方向上,我們連接從 scrollView.leading - textField1 - scrollView.trailing ;(不管是 1 還是 2,這里只需要一個 textfield 就可以)垂直方向上,連接 scrollView.top - textField1 - textField2 - scrollView.bottom;

是不是真的這樣呢,我們添加了一堆約束,發現仍然是一段紅線,怎么辦,這里給大家推薦一個辦法,看一下系統是怎么干的。我們使用 add missing constraints ,看系統給我們添加了哪些約束,然后進行分析。

系統自動添加約束

約束分析

  • imageView 和 scrollView 本身的約束并沒有變化,符合預期
  • textField 被設置了約束,并且所有約束成功的「撐滿了」scrollView 的 content

接下來就分析下 UITextField 上的幾個約束是如何「撐滿」content 的:

需要注意的是,textField 本身已經存在了 height 和 width 上的約束,但是你可能在截圖看不出來,是因為我在 Xcode 中 開啟了 Intrinsic size 為 placeholder ,就是說 textField 現在已經有了固定內容。

首先分析第一個 textField:

// 以下的 superView 即 scrollView

// 水平居中
textField-1.centerX= superView.centerX

// 距離右邊緣,設定一個絕對值 
textField-1.trailing + 131 = superView.trailing

// 距離上邊緣,設定絕對值
textFeild-1.top = superView.top + 163

// 距離下邊緣,設定絕對值
textField-1.bottom + 20 = textField-2.bottom // note

/**
*   可以看出,由上面這些約束可以完整的推斷出 scrollView 的 content size:
*   由 水平居中、距離右邊緣 131、本身寬度 100,得出 content.size.width = (50 + 131)* 2
*   由 距離上邊緣 163、距離下面的 textField-2 20、本身高度 30, 得出 content.size.height = 163 + 30 + 20 + textField-2距離下邊緣值
*   可以看出 content size 的 width 已經確定了,但是 height 還依賴于 textField-2 與下邊緣的距離
*/

接下來分析第二個 textField-2:

// top 
textField-2.top = textField-1.bottom + 20

// leading
// textField 1 和 2 跟左邊緣的距離相等,左側對齊
textField-2.leading = textField-2.leading

// trailing
// 因為 textField-1 的約束已經確定了 contentSizeS.width ,且 textField-2 的 width 固定。因此 trailing 方向上可以不需要約束

// bottom
textField-2.bottom + 20 = superView.bottom

結論

  • scrollView 出現 ambiguity 約束警告的原因是,在 scrollView 中的 subView 不能確定 content size 的大小。
  • 只要 scrollView 中的 subViews 的約束能夠唯一的確定 content size ,那么就不會出現問題
  • 當 content size 的 width 超出了 scrollView 本身的 width,即可水平方向滾動
  • 當 content size 的 height 超出了 scrollView 本身的 height ,即可垂直方向滾動

如果我想要滾動呢?

由以上我們知道,如果我們想要讓 scrollView 聽話,需要解決掉約束的歧義,即確定 scrollView 的 content size。

確定 content size 的必要條件是,分別在水平方向和垂直方向都能有一些約束組合起來確定當前 content size 的 height 和 width。

也就是說,content size 其實由約束來決定的,你可以認為 scrollView 中的那塊畫布是無限伸展的。不信你試試。一般情況下,下圖中,當 scrollView 是普通的 UIView 時,確定左邊距以及頂部邊距之后,本身又有固定的高度和寬度,最右邊的紅色箭頭指向的那條約束是不必要的,想想為什么?

因為在普通的 view 中,view 的寬度已經固定了,subView 只要確定了左邊距和本身的寬度,我用普通 view 自己的寬度減去前面兩者,就自己能推斷出來,subView 距離右邊的距離了。

但是 scrollView 為什么還得繼續加一個右邊距呢,特殊的原因之前已經提到了。scrollView 的 content 不是自己決定的,而是由 subView 決定,因此你得加個右邊距,scrollView 才會知道自己的 content size 的寬度,向右的邊距大于 scrollView 的 frame 的寬度時,就可以向右邊滾動了。你看,我在這里設置了右邊距為 400。

伸展的 scrollView

scrollView origin 在 y 軸上 64 個像素的下移

也許你有時候會發現,scrollView 中的 content 頂部并是不嚴格的貼緊 scrollView 的頂部。使用 Xcode 的 Debug View hierarchy 功能,可以看到有大概 64 個像素的下移。如下圖右上角:

scroll view content 下移 64 像素

這其實應該是一個歷史遺留問題,只要 scrollView 是 ViewController 的第一個 subView,且導航欄不隱藏時,UIViewController 在 iOS 7 下,會自動將視圖中的 scrollView 進行調整,API 是這樣的:

// bool 值,指明是否自動去調整 scrollView 的 contentInsets

@property(nonatomic,assign) BOOL automaticallyAdjustsScrollViewInsets NS_AVAILABLE_IOS(7_0); // Defaults to YES

解決辦法

  • 在 iOS7 之下設置上面的 API 為 NO
  • 不要讓 scrollView 作為 viewController 的第一個子視圖

關于 scrollView 64 point 偏移可以去了解這些:

http://corsarus.com/2014/uiscrollview-basics/

http://segmentfault.com/a/1190000002936987

更加優雅的解決方案

如果在添加 subView 的約束時,需要不斷的考慮其 scrollView 的 content ,是一件很麻煩的事情,所以得想個辦法弄的優雅一些。目前比較常用的是給 scrollView 添加一個覆蓋全部范圍的 UIView 或者 containerView,然后在這個視圖種添加真正的 subView,這樣子就可以讓 subView 針對這個 view 來布局。

添加 UIView

1、給 UIScrollView 添加 UIView ,命名為 contentView 設置約束上下左右邊距都為0 ,鋪滿 scrollView 窗口。

2、然后在 UIScrollView 中進行布局真正的 subView。

然后你會發現,仍然報紅線約束歧義,這是因為雖然我們給 scrollView 添加了一層,但是并沒有解決基本問題:scrollView 仍然不能確定自己的 content size 是多少?

為了解決上面的問題,我們給 contentView 添加兩個約束:

conentView.width = scrollView.width
conentView.height = scrollView.height

這樣子,UIScrollView 就知道自己的 content size 了,即 contentView 的大小。現在你在 contentView 中的布局都能夠剛好鋪滿 scrollView。

問題解決的不徹底怎么能叫填坑呢,有同學會問,如果我的內容超過了一屏,想要滑動怎么辦。修改約束的 multiplier 或者 constant 即可。想要水平滑動,修改 conentView.width = scrollView.width這條約束的 multiplier 或者 constant 即可。因為你修改了這個值后,contentView 的寬度也就變化了,即 scrollView 的可滑動區域 content size 也就修改了。

其實,conentView.width = scrollView.width 這條約束寫完整是,conentView.width = scrollView.width * multiplier + constant,如果對 autoLayout 不了解的同學還需再次查漏補缺。我們還可以將這個約束拖到代碼中作為 IBOutlet。

鍵盤遮擋事件

如果 scrollView 中存在 textField ,且可能會被遮擋鍵盤時,可以這樣解決:

1、創建對象保存當前被用戶點擊的 textField

2、監聽鍵盤的調起和放下事件

3、在調起事件中,首先判斷當前的 textField 會不會被遮擋,被遮擋時,修改 scrollView 的 contentInset ,并且判斷修改之后,當前 textField 是否還在可視區域內,如果不可視還需調用 - scrollRectToVisible:animated: 滑動到可視區域內。

4、在鍵盤消失事件中,恢復 scrollView 的 contentInset 值

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容