iOS進階指南試讀之UI篇
UI篇
UI是一個iOS開發工程師的基本功。
怎麼說?
UI本質上就是你調用蘋果提供給你的API來完成設計師的設計。
是以,想提升UI的功力也很簡單,沒事就看看UIKit裡的各個類的頭檔案。如果能做到爛熟于胸,相信會有很大的提升。
顧名思義,Autolayout = 自動+布局,也就是當你設定好一定的限制之後,系統會幫你處理布局的細節。
那麼,在不那麼自動的年代,我們用的是什麼?
我們用的是Frame布局。
那麼,先來讨論一下Frame布局有哪些問題?
舉個簡單的例子好了。
如圖。
代碼如下。
圖中黃色的View是紅色View的子View,那麼,如果我期望無論紅色View變大還是變小,黃色View距離紅色View的邊距總是不變的,該怎麼做呢?
一般來說有兩種做法。
設定黃色View的<code>autoresizingMask</code>屬性,設定為<code>UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight</code>,這樣設定的結果是黃色View的寬高會随着父View寬高的改變而改變,但是不改變間距。
繼承一個UIView,在UIView裡的<code>layoutSubviews</code>裡設定子View的寬高。如下。
實際上這兩種做法都是很麻煩而且不靈活的。例如<code>autoresizingMask</code>對應的<code>UIViewAutoresizing</code>模式隻有6種,我們常常用到的居中對齊等完全沒有。二是如果在<code>layoutSubviews</code>裡設定布局,又會造成如果父View有動畫,那麼會出現奇怪的動畫效果。
是以,為了解除這些痛點,蘋果公司為我們帶來了Autolayout。
在這裡,我會假設看本書的都是會使用Autolayout基本功能的朋友。是以,我會直接講解一些較為深入和偏實戰的東西。
思考一下,當你拉限制的時候,為什麼UILabel和UIButton隻需要拉能夠确定坐标的限制即可,而不需要确定寬高的限制?比如說。
這個UILabel就是隻确定了左邊距和上邊距,就可以了。但是如果我們放了一個UIView,隻确定坐标而不指定大小,就會出錯。例如。
Xcode會提示,這個View還需要指定寬和高。
造成這種情況的原因就是,有些View可以通過自己的内容計算寬高,而有些View不可以。這也是我們要講的内容,也即我們的标題,<code>Intrinsic Content Size</code>,翻譯過來就是,固有内容大小。
對應的系統方法就是<code>- (CGSize)intrinsicContentSize</code>
要知道,Autolayout的本質無非就是系統通過你設定的限制來幫你計算一個控件的位置和大小。
是以,一個UILabel肯定具備的四個條件是,内容、字型、行數、換行模式。也就是說,隻要我們賦予了Label内容,那麼它的大小也就确定了。是以,我們不需要特意指定一個UIlabel的寬高,除非你有什麼特殊要求。
那麼UIView為什麼不可以自己計算大小?
答案其實也可以猜到,因為他沒有内容。
來看一個例子。
首先,我們在storyboard上放置一個UILabel,然後設定Leading和Trailing。如圖。
運作模拟器,看一下效果。
我們會發現,這樣設定的UILabel,它的大小總是會和内容大小剛好一緻,但是如果我們期望UILabel的大小總是比内容寬高都大一些,也就是所謂的留白。比如這樣。
那麼,我們應該怎麼做呢?
首先,我們建立一個繼承于UILabel的自定義試圖,然後重寫
上述代碼的意思就是,我們先擷取系統通過Label的内容計算出來的寬和高,再分别給他增大再傳回新的Size就可以了。再運作一下,你就會發現,Label的大小就會比内容大了。(别忘了,把對齊方式設定為居中)
再回到之前的那個問題,UIView如果隻設定坐标,不設定大小會報錯的問題。
如果是用代碼寫限制,如果你隻想設定坐标不想設定大小,那麼你需要像上面的代碼一樣,在<code>- (CGSize)intrinsicContentSize</code>為你的UIView指定一個預設大小。
如果是在XIB裡,那麼你需要在下圖這個<code>Instrinsic Size</code>的屬性裡設定為<code>Placeholder</code>。這樣,Xcode就不會報錯了。
其實看完上面的叙述,你會思考,到底什麼情況下,一個UIView需要隻設定坐标不設定大小呢?
其實這種場景相當普遍。比如,我們常常會碰到,一個View中有兩個Label,兩個Label的高度均和内容有關,這時候,你的View的高度就必須由兩個Label的高度有關,而不能一開始就定死。例如。
一個已知寬度的UIView中,有兩個UILabel,我希望這個UIView的高度由兩個UIlabel的高度來确定。效果如下圖。
也就是說,圖中紅色view的高度是和兩個UIlabel相關聯的。我們嘗試來實作它。
首先,在storyboard上拉取一個UIView。設定背景色為紅色。如圖。
我們為這個紅色View設定了3個限制,分别是。
Leading space to SuperView:8
Trailing space to SuperView:8
Top Space to SuperView:8
也就是分别設定了View的左邊距,右邊距和上邊距,熟悉限制的人應該知道,這時候View的限制是不夠的。為什麼?
因為,左邊距和上邊距确定了View的(x,y),然後左邊距和右邊距确定了這個View的寬度,我們缺少了Height。
但是這個Height,要由View内部的兩個label來确定,為了讓Xcode不再認為我們拉的限制有問題,再結合我們上面講的<code>Intrinsic Content Size</code>,我們可以在Xib的這個位置設定<code>Intrinsic Size</code>為<code>Placeholder</code>,這樣,Xcode就認為這個View有預設的大小,是以就不會報錯了。
然後,自然是放入兩個UILabel了。如圖。
為了label能夠多行顯示,别忘記設定lineofnumber為0.
還有一點需要注意的是,兩個UILabel之間肯定是需要一個設定一個垂直限制的,否則整個View就沒有辦法确定自己的高度,思考一下這是為什麼?
運作一下,看看結果。
成功了。
這兩個概念需要結合我們上面講的<code>Intrinsic Content Size</code>來了解,每一個控件都有一個系統計算的最佳大小。
是以,<code>Content Hugging Priority</code>這個屬性就代表着,一個控件拒絕本身size大于<code>InstrinsicSize</code>的優先級。
那麼<code>Content Compression Resistance</code>,這個屬性代表着,一個控件拒絕本身size小于<code>InstrinsicSize</code>的優先級。
這樣子說還是有點抽象。那麼我們來看一個例子好了。
我們在storyboard中建立了一個UITableView,并在其中建立了一個自定義的UITableViewCell。
這個自定義的UITableviewCell中有兩個Label,都是多行顯示的Label。然後給他們設定限制。
第一個Label設定的限制為:
Leading Space:8
Trailing Sace:8
Top Space:8
Bottom Space(距離下面的Label的間距): 9
第二個Label設定的限制為:
Bottom Space:8
Top Space(距離上面面的Label的間距): 9
設定好了,ok,好像沒什麼問題。如圖。
但是,如果這時候你把UITableViewCell的高度擴大。看看是怎樣的結果。如圖。
Xcode報錯了。為什麼?
因為Cell的高度擴大,勢必會影響兩個Label的位置和大小,是以,現在Label面臨着一個問題,在保持和父View(也就是UITableViewCell的contentView)間距不變的情況下,必須有一個Label是需要妥協的,怎麼個妥協法呢?就是要擴大高度,擴大高度,就意味着比label本身的文字内容的高要大了。
那麼,到底是兩個Label中的哪一個label做出這個妥協呢?
Xcode并不知道,因為這個不知道,是以Xcode報錯了。
話說回來了,Xcode為什麼會不知道,就是因為兩個Label的<code>Content Hugging Priority</code>裡面的<code>Vertical</code>這個屬性的值是一樣的。因為兩個Label拒絕變高的優先級相同,以至于Xcode不知道到底該拉伸哪個Label。是以,解決方案就是,把其中一個的改小。如圖。
這樣就可以了。那麼,這樣就結束了麼?
如果這時候,你再把UITableViewCell的高度減小,會發生什麼情況呢?Xcode又報錯了。那麼,這次的原因又是什麼?
其實和上一個原因較為類似。
Cell的高度減小導緻UILabel為了保持和父View間距不變,是以面臨一個高度壓縮的情況,那麼到底誰壓縮,Xcode依然不知道,因為兩個Label的<code>Content Compression Resistance</code>這個屬性裡的<code>Vertical</code>優先級一樣,和上面的解決辦法一樣隻需要改小一個即可。
其實這個東西我在Reviewcode.cn裡讨論過。但是鑒于可能會有人沒有看過,并且對這個問題存疑,還是在這裡說一下。
結論如下:
如果是在自定義view中,寫在init方法中。
如果是在ViewController中,寫在<code>- (void)viewDidLoad()</code>中。
為什麼不能寫在<code>viewDidAppear</code>或者<code>viewWillAppear</code>中?
因為這個東西和<code>NSNotification</code>是一樣的,你不能确定<code>viewDidAppear</code>和<code>viewWillAppear</code>調用的時機和調用的次序,不信的話,你可以用NSLog列印一下,并且使用手勢在NavigationController中不停的左右滑動控制器,看看列印的結果。
但,<code>viewDidLoad</code>是可以保證在整個生命周期隻出現一次的。為了避免限制重複添加,是以你應該在<code>viewDidLoad</code>中添加。