家家有本難念的經。現在有一個新需求,要你在運行時增加或刪除一個視圖,這就需要我們重新調整鄰近視圖的位置。
你會怎麼辦?也許你會在故事板中新建一些布局約束連接,以便能夠安裝或卸載其中的一些約束?或者你會使用第三方庫來實現?或者看任務復雜程度完全用代碼實現?
也許這個在視圖附近的視圖樹中的所有View都不需要在運行時改變,但當你將新視圖添加到故事板時,仍然要想方設法為它擠出空間來。
你是否有過這樣的時候:因為覺得要解決的布局約束沖突太過棘手,最終不得不清除所有約束並一條條地重新添加約束。
通過對UIStackView的介紹,完成上述任務變得微不足道。Stack View提供了一個對多個視圖進行水平或垂直布局的方法。通過對幾個屬性進行簡單設置,比如對齊、分布和間距,可以讓我們讓其所包含的視圖適應於其有效空間。
注意:UIStackView教程假設你基本掌握了自動布局。如果你還不熟悉自動布局,請參考“自動布局教程:第1部分”。
在本教程,你將創建一個名為Vacation Spots的App。這個App非常簡單,僅僅顯示了一個能讓你原離一切塵囂的景點列表。
先不要忙著打點行裝,因為這個App還有幾個問題需要你用Stack View來進行解決,當然,這比你光用自動布局來解決要簡單多了。
首先下載本教程的開始項目,然後在iPhone6S 上運行。
點擊London單元格,跳轉到倫敦的介紹頁面。
一瞥之下,這個頁面似乎是OK的,但它其實存在著這幾方面的問題。
查看視圖下方的那排按鈕。它們當前的位置是以固定的間距排列的,因此它們無法適應屏幕寬度的變化。要看出問題之所在,你只需按下command+左箭頭旋轉模擬器屏幕為橫屏即可。
點擊WEATHER旁邊的Hide按鈕。它成功地隱藏了文本,但它下面的內容卻保持在原來的位置,從而留下了一大塊的空白區域。
各版塊的順序也應當調整。如果將”what to see”版塊放到”why visit”版塊的右邊,而不是將”wheather”版塊插在兩者之間會更好。
在橫屏模式下,底部的一排按鈕太靠下了。如果在橫屏模式下,將各版塊之間的距離縮小一點會更好。
現在,你已經了解了將要做的工作,是該進入項目中進行工作的時候了。
打開Main.storyboard,找到Spot Info View Controller這個Scene。哇!在你的Stack View上有很多顏色!
將這些標簽和按鈕設為不同的背景色,是為了在運行時效果更直觀。就是在故事板中,這也有助於看到Stack View屬性的改變導致其內部視圖的變化。
如果你想在運行App時看見這些顏色,你可以在SpotInfoViewController的viewDidLoad()方法中將下列語句注釋。當然現在,還不需要這樣做:
// Clear background colors from labels and buttons
// 清空標簽和按鈕的背景色
for view in backgroundColoredViews {
view.backgroundColor = UIColor.clearColor()
}
同時,所有已連接Outlet的標簽都會有一個提示文本,用於指明它們所連接的Outlet變量名。這將在運行時指明是哪個標簽。例如,帶有字樣的標簽將連接到下面這個Outlet變量:
@IBOutlet weak var whyVisitLabel: UILabel!
還要注意另外一點,故事板中Scene的大小並不是默認的啟用了Size類時的600x600。
Size類仍然可用,但在初始導航控制器的屬性面板中,Simulated Metrics下的Size屬性卻被設置成了iPhone 4-inch。這僅僅是為了讓我們更容易使用故事板一點,Simulated Metrics屬性在運行時並沒有任何影響——不同設備上視圖的大小仍然會自動改變。
第一個Stack View
我們首先要做的是底下一排按鈕之間的間距。一個Stack View能夠將它所含的View以各種方式沿其軸向進行分布,同時也可以將View沿某個方向等距分布。
幸運的是,將已有的View添加到一個新的Stack View中並不是一件多復雜的事情。首先,用Command+左鍵同時選中Spot Info View Controller底下一排的所有按鈕:
如果Outline視圖未打開,先點擊故事板畫布左下角的Show Document Outline按鈕,打開Outline視圖:
在Outline視圖中查看是否3個按鈕都已經選中:
如果沒有全部選中,你仍然可以用Command+左鍵在Outline視圖中重新選擇它們。
一旦選好這3個按鈕,找到故事板畫布左下角Auto Layout工具欄中新增的Stack按鈕並點擊它:
這些按鈕將被嵌到一個新的Stack View中:
3個按鈕現在一個擠著一個——你將很快解決這個問題。
雖然Stack View會管理這些按鈕的位置,但它自身仍然需要我們為它添加自動布局約束。
當你將View添加到Stack View中時,任何相對於其他視圖的約束都被刪除。以之前添加的按鈕為例,Submit Rating按鈕上方有一個相對於Rating標簽底部的垂直間距約束:
現在點擊Submit Rating 按鈕,可以看到它已經丟失了所有約束:
另一種檢驗這些約束已經刪除的方法是用Size面板進行查看(??5):
為了給Stack View添加布局約束,首先需要選中它。要在故事板選取一個充滿了子視圖的Stack View還是比較難的。
一個簡單的法子是在Outline視圖中選取Stack View:
另一個技巧是在Stack View 的任意地方按下Shift+右鍵或者Control+Shift+左鍵(如果你正在用觸控板的話)。這時將彈出一個上下文菜單,列出了位於所點擊的地方的View樹,你可以在這個菜單中選擇Stack View。
現在,我們用Shift+右鍵的方式選取Stack View:
然後點擊自動布局工具欄中的Pin按鈕,添加一個約束:
首先勾選Constrain to margins。然後在Stack View四周添加下列約束:
Top: 20, Leading: 0, Trailing: 0, Bottom: 0
仔細檢查top、leading、trailing、bottom中的數字並確保它們的I型柱都被選中。然後點擊Add 4 Constraints:
現在Stack View的尺寸正確了,但它會拉伸第一個按鈕的大小使其填充所有剩余空間:
決定Stack View如何將它的subview沿軸向分布的屬性是它的distribution屬性。當前,這個屬性的值是Fill,這表示subview會沿軸向完全占據Stack View。因此,Stack View會拉伸其中一個subview使其填充剩余空間,尤其是水平內容優先級最低的那個,如果所有subview優先級相同,則拉伸第一個subview。
當然,你並不希望按鈕將整個Stack View都填充——你想讓它們等間距分布。
確保Stack View處於選中狀態,打開屬性面板。將Distribution屬性由Fill修改為Equal Spacing:
編譯運行,點擊某個單元格,旋轉模擬器(?→)。你將看到最下一排按鈕現在按照等間距排列了!
如果不使用Stack View,則解決這個問題只能使用空白的View來分隔這些按鈕,在按鈕之間擺放上一些用於分隔空間的 Spacer View。所有的Spacer View都要添加等寬約束,以及許多額外的約束,才能將這些Spacer View布局正確。
這看起來如下圖所示。為了直觀起見,這些Spacer View的背景色設置成了淺灰色:
一旦你不得不在故事板中這樣做的時候,就會導致出許多問題,尤其是許多視圖都是動態的時候。如果要在運行時添加一個按鈕或者隱藏/刪除一個按鈕時,要想調整這些Spacer View和約束真的是不太容易。
要隱藏Stack View中的視圖,你只需要設置該View的Hidden屬性為true,剩下的工作Stack View會自己完成。這也是我們解決用戶隱藏WEATHER標簽下文本的主要思路。在本文後面的內容中,當你將weather標簽添加到Stack View之後,我們再來介紹這個問題。
將各版塊轉換到Stack View
我們將把SpotInfoViewController中的所有版塊都添加到Stack View中。這將有助於我們完成剩下的任務。接下來我們先調整Rating版塊。
Rating版塊
就在前面創建的Stack View上方,選取RATING標簽,以及旁邊的顯示為幾個星形圖標的標簽:
然後點擊Stack按鈕將它們嵌到一個Stack View中:
然後點擊Pin按鈕。勾選Constrain to margins,並添加如下約束:
Top: 20, Leading: 0, Bottom: 20
打開屬性面板,將間距設置為8:
你可能會看到一個 Misplaced Views的布局約束警告,同時星星標簽會顯示將會被拉伸到視圖之外:
有時候Xcode會臨時提示一些警告,或者顯示Stack View的位置不正確,這些警告會在你添加其他約束後消失。你完全可以忽略這些警告。
要解決這個警告,我們可以修改一下Stack View的Frame然後又改回,或者臨時修改它的一條布局約束。
讓我們試一下。先將Alignment 屬性從Fill修改為Top,然後又改回原來的Fill。你將看到這下星星標簽顯示正常了:
編譯運行,進行測試。
解散一個Stack View
在繼續後面的內容之前,先接受一些“急救”訓練。有時你發現一些Stack View不再需要了,可能因為過時了,代碼進行了重構或者僅僅是突發奇想。
幸運的是,有一種簡單的方法可以解散一個Stack View。
首先,選定想解散的Stack View。按下Option鍵,點擊Stack 按鈕。這將彈出一個上下文菜單,然後點擊Unembed:
另一種解散的方法是選中Stack View,然後點擊EditorUnemebed菜單。
垂直的Stack View
接下來,你將創建一個垂直的Stack View。選中WHY VISIT標簽及下面的標簽:
Xcode會自動根據這兩者的位置推斷出這將是一個垂直的Stack View。點擊Stack 按鈕將二者嵌到一個Stack View:
下面的那個標簽原先有一個right margin of the view的約束,但嵌到Stack View之後被移除了。現在,這個Stack View還沒有約束,因此它自動適應了兩個標簽中的最寬的一個的寬度。
選中Stack View,點擊Pin按鈕。勾選Constrain to margins,設置Top、Leading、Trainling為0。
然後,點擊Bottom右邊的下拉按鈕,從列表中選擇WEATHER(curent distance =20):
默認,約束是相對於距離最近的對象,對於Bottom約束來說就是距離它15像素的Hide按鈕。但我們其實是想讓約束相對於WEATHER標簽。
最後點擊Add 4 Constraints按鈕。顯示結果如下圖所示:
現在,我們有了第二個Stack View,它的右邊對齊於View的右邊。但是底下的標簽仍然是原來的寬度。你接下來要修改Stack View的alignment屬性以解決這個問題。
Alignment屬性
Alignment屬性決定了Stack View如何沿它軸向的垂直方向擺放它的subview。對於一個垂直的Stack View,這個屬性可以設置為Fill、Leading、Center和Trailing。
對於水平的Stack View,這個屬性則稍有不同:
.Top取代了.Leading,.Bottom取代了.Trailing。此外,水平Stack View還多出了兩個屬性值:.FirstBaseLine和.LastBaseLine。
這是對於垂直Stack View,將Alignment設置為不同屬性值所造成的顯示效果:
Fill:
Leading:
Center:
Trailing:
當你測試完所有Alignment值的布局效果後,將Alignment修改為Fill:
然後編譯運行,測試是否正常。
將Alignment設置為Fill,表示所有View將沿與Stack View軸向垂直的方向進行全占式分布。這會讓WHY VISIT標簽擴展它的寬度到100%.
如果我們只想讓底下的標簽將寬度擴展到100%怎麼辦?
這個問題現在看來還不是多大的問題,因為兩個標簽在運行時的背景色都是透明的。但對於Weather版塊來說就不同了。
我們將用另外一個Stack View來說明這個問題。
將”What to see”換成Stack View
這個版塊和前面一個版塊類似,因此我們會簡述。
1.首先,選中 WHAT TO SEE標簽和標簽。
2.點擊Stack按鈕。
3.點擊Pin 按鈕。
4.勾選Constrain to margins,添加4個約束:
Top: 20, Leading: 0, Trailing: 0, Bottom: 20
5.將Stack View的Alignment設置為Fill。
現在你的故事板看起來像這樣:
編譯運行,測試是否有任何改變。
現在就只剩下weather版塊了。
將Weather版塊轉換成Stack View
Weather版塊相對復雜一些,因為它多了一個Hide按鈕。
一種方法是使用嵌套的Stack View,先將WEATHER標簽和Hide按鈕嵌到一個水平StackView,再將這個Stack View和標簽嵌到一個垂直Stack View。
看起來是這個樣子:
注意,WEATHER標簽被拉伸為和Hide按鈕一樣高了。這並不合適,因為這會導致WEATHER標簽和下面的文本之間多出了一些空間。
注意Alignment屬性負責Stack View軸向垂直的方向上的布局。所以,我們需要將Alignment屬性設置為 Bottom:
但我們並不想讓Hide按鈕的高度來決定Stack View的高度。
正確的方法是讓Hide 按鈕不要和Weather 版塊呆在同一個Stack View中,或者任何別的Stack View中。
這樣,在頂層View中還會保留一個subview,你將為它添加一個相對於WEATHER標簽的約束——WEATHER標簽嵌在Stack View裡的。也就是說,你要為位於Stack View之外的按鈕加一個約束,這個約束是相對於Stack View內的一個標簽!
選中WEATHER標簽和標簽:
點擊 Stack 按鈕:
點擊Pin 按鈕,勾上Constrain to margins,然後添加如下約束:
Top: 20, Leading: 0, Trailing: 0, Bottom: 20
將Stack View的Alignment設為Fill :
我們需要在 Hide 按鈕左邊和WEATHER標簽右邊加一條約束,這樣WEATHER 標簽的寬度就不會拉滿整個Stack View了。
當然,底下的標簽寬度還是需要100%占滿的。
我們是通過將WEATHER標簽嵌到一個垂直Stack View 來實現的。注意,垂直Stack View的Alignment 屬性可以設置為 .Leading,如果將Stack View拉寬,則它裡面的View 會保持左對齊。
從Outline視圖中選取WEATHER 標簽,或者用Control+Shift+左鍵的方式選取WEATHER 標簽:
然後點擊Stack 按鈕:
確保Axis 為 Vertical 的情況下,將Alignment 設置為 Leading:
好極了!這樣外面的Stack View 會將內部的 Stack View 拉伸為完全填充,而內部的 Stack View 則允許標簽保持原有寬度!
編譯運行。這究竟是為什麼?Hide按鈕跑到了文字中間去了?
這是因為WEATHER標簽是嵌在 Stack View 中的,任何它和 Hide 按鈕之間的約束都被刪除了。
從Hide 按鈕用右鍵拖一條新的約束到 WEATHER 標簽:
按下Shift鍵,同時選擇Horizontal Spacing 和 Baseline。然後點擊 Add Constraints:
編譯運行。Hide 按鈕的位置現在對了,而且當按下Hide 按鈕,位於Stack View 中的標簽被隱藏後,下面的視圖也會被調整——根本不需要我們進行手動調整。
選擇是有的版塊都在自己的 Stack View中,我們將在它們外邊在套上一個 Stack View,這樣最後的兩個任務就會變得很輕松。
頂級 Stack View
在Outline 視圖中,用Command+左鍵選擇5個最頂級的 Stack View:
然後點擊 Stack 按鈕:
點擊Pin 按鈕,勾上 Constrain to margins,將 4 個邊的約束都設為0。然後將Spacing 設置為20,Alignment 設為 Fill。現在故事板會是這個樣子:
編譯運行:
噢!這個 Hide 按鈕又失去了它 的約束!因為包含 WEATHER 標簽的Stack View的外邊又套了一層 Stack View。這不是什麼大問題,就像之前你做過的那樣,再重新為它添加約束就是了。
右鍵從Hide 按鈕拖一條約束到 WEATHER標簽,按下 Shift 鍵,同時選擇 Horizontal Spacing 和 Baseline。然後點擊 Add Constraints:
編譯運行。Hide按鈕的位置正確了。
重新調整視圖位置
現在,所有的版塊都被嵌到一個頂級的 Stack View中了,我們想修改一下 what to see版塊的位置,讓它位於 weather 版塊之後。
從 Outline 視圖中選擇中間的的 Stack View,然後將它拖到第一、二個 Stack View 之間。
Note: Keep the pointer slightly to the left of the stack views that
you’re dragging it between so that it remains a subview of the outer
stack view. The little blue circle should be positioned at the left
edge between the two stack views and not at the right edge:
注意:讓箭頭稍微偏向你正在拖的Stack View左邊一點,以便它能夠作為外層 Stack View 的 subview 添加。藍色的小圓圈應當位於兩個 Stack View 之間的左端而不是右端:
現在,weather版塊是從上到下的第三個版塊,由於 Hide 按鈕它並不是 Stack View的subview,所以它不會參與移動,它的frame當前是不正確的。
點擊 Hide 按鈕,選中它:
然後點擊自動布局工具欄中的 Resolve Auto Layout Issues 按鈕,選擇 Update Frames:
現在 Hide 按鈕將回到正確的位置:
好,對於你來說,用自動布局調整視圖的位置或者重新添加約束都不再是什麼問題了,但為什麼還是有一種不太完美的感覺呢?
基於配置的 Size 類
最後還有一個任務沒有完成。在橫屏模式,垂直空間是比較珍貴的,你想將這些版塊之間靠得更近一些。要實現這個,你需要判斷當垂直Size類為compact時,將頂層 Stack View的 Spacing屬性由 20 改成 10.
選擇頂層 Stack View,點擊 Spacing 前面的 + 按鈕:
選擇 Any Width > Compact Height:
在新出現的 wAny hC 一欄中,將 Spacing 設為 10:
編譯運行。在豎屏模式下Spacing不會改變。旋轉模擬器(?←),你會看到各版塊之間的間距減少了,現在底部按鈕之間的空間也變大了:
如果你沒有添加最外層的 Stack View,你仍然可以使用 Size 類將每個版塊之間的垂直間距設置為 10,但這就不是僅僅設置一個地方就能夠辦到的了。
剩下的時間將會有更加愉快的事情發生,那就是動畫!
動畫
現在,在隱藏和顯示天氣信息時仍然會覺得有一些突兀。你將增加一個動畫使這個轉換變得更平滑。
Stack View完全支持 UIView 動畫。也就是說要以動畫方式顯示/隱藏它所包含的subview,只需要簡單地在一個動畫塊中切換它的 hidden 屬性。
讓我們來看看代碼怎麼實現。打開 SpotInfoViewController.swift,找到
updateWeatherInfoViews(hideWeatherInfo:animated:)方法。
將方法的最後一行:
weatherInfoLabel.hidden = shouldHideWeatherInfo
替換為:
if animated {
UIView.animateWithDuration(0.3) {
self.weatherInfoLabel.hidden = shouldHideWeatherInfo
}
} else {
weatherInfoLabel.hidden = shouldHideWeatherInfo
}
編譯運行,點擊Hide 按鈕或 Show 按鈕。是不是加入動畫之後看起來要好得多呢?
除了對 Stack View 中的視圖以動畫的方式設置 hidden 屬性,你也可以對 Stack View 自身的屬性使用 UIView 動畫,例如 Alignment 屬性、 Distribution 屬性、 Spacing 屬性和 Axis 屬性。
接下來做什麼
你可以從這裡下載完整的示例項目。
在這篇 UIStackView 教程裡,你學習了什麼是 Stack View,以及它的各個屬性,通過這些屬性可以對它的 subview 進行布局。Stack View 是高度可配置的,要達到同樣的效果往往不止一種實現方式。
最好的學習方式是自己修改各種屬性並體驗最終效果。不要一個屬性一個屬性地單獨測試,而是要試驗各種屬性相互搭配後的布局效果。
本教程是”iOS 9 Tutorials”第6章”UIStackView and Auto Layout changes”,以及第7章”Intermediate UIStackView”的精簡版。如果你想學習更多關於 UIStackView以及其它 iOS9 的新特性,請看這本書!
同時,如果你對本文有任何問題和建議,請加入到下面的討論中來!