在這本書中,您一直在使用自動布局來創(chuàng)建靈活的界面,可以跨設(shè)備類型和大小進(jìn)行擴(kuò)展。 自動布局是一種非常強(qiáng)大的技術(shù),但是這種能力帶來了復(fù)雜性。 布置一個界面通常需要大量的約束,并且由于需要不斷地添加和移除約束,所以創(chuàng)建動態(tài)界面是很困難的。
通常,界面(或界面的子部分)可以以線性方式布局。 考慮一下您編寫的其他應(yīng)用程序:第1章的 Quiz
應(yīng)用程序由四個子視圖組成,它們是垂直布局的。 對于 WorldTrotter
應(yīng)用程序來說也是如此; ConversionViewController 有一個垂直界面,由一個文本字段和幾個標(biāo)簽組成。
具有線性布局的界面非常適合使用 棧視圖(stack view)
。 棧視圖是 UIStackView 的一個實例,它允許您創(chuàng)建一個垂直的或水平的布局,它很容易布局,并且會自動管理您通常需要自己去管理的大部分約束。 也許最重要的是,您可以在其他棧視圖中嵌套棧視圖,這使您能夠在很短的時間內(nèi)創(chuàng)建真正令人驚嘆的界面。
在本章中,您將繼續(xù)使用 Homepwner
創(chuàng)建一個界面,以顯示特定 Item 的詳細(xì)信息。 您創(chuàng)建的界面將由多個嵌套的棧視圖組成,包括垂直和水平的視圖(圖13.1)。
圖13.1 Homepwner和堆棧視圖
使用UIStackView
您將創(chuàng)建一個用于編輯 Item 詳細(xì)信息的界面。 您將在本章中完成基本界面,然后您將在第14章中完成實施細(xì)節(jié)。
在頂層,您將擁有一個垂直的堆棧視圖,其中包含四個元素,顯示 item 的名稱(Name)、序列號(Serial)、值(Value)和創(chuàng)建日期(Date Created)(圖13.2)。
圖13.2 垂直棧視圖布局
打開您的 Homepwner
項目,然后打開 Main.storyboard
。 將一個新的 View Controller
從對象庫拖到畫布上。 將 Vertical Stack View
從對象庫拖動到 View Controller
的 View
上。 向棧視圖添加約束上下左右固定為 8
點(diǎn)。
現(xiàn)在將 UILabel 的 4 個實例從對象庫拖放到棧視圖中。 從上到下,給這些標(biāo)簽添加文本 “Name”、“Serial”、“Value” 和 “Date Created”(圖13.3)。
圖13.3添加標(biāo)簽到棧視圖中
您可以馬上看到問題:標(biāo)簽都有一個紅色邊框(表示自動布局問題),并且有一個警告,一些視圖是垂直不明確的。 有兩種方法可以解決這個問題:使用自動布局或修改棧視圖的屬性。 我們首先使用自動布局解決方案,因為它突出顯示了自動布局的重要性。
隱式約束
你在第3章學(xué)到,每個視圖都有一個固有的內(nèi)容大小。 您還了解到,如果不指定明確確定寬度或高度的約束,則視圖將從其固有內(nèi)容大小中派生出其寬度或高度。 這是如何實現(xiàn)的呢?
它使用從視圖的 content hugging priorities
和 content compression resistance priorities
的隱式約束來完成這一任務(wù)。 每個視圖(View)的每個軸都有以下優(yōu)先級之一:
horizontal content hugging priority
vertical content hugging priority
horizontal content compression resistance priority
vertical content compression resistance priority
Content hugging priorities
Content hugging priorities
的內(nèi)容就像放置在視圖周圍的橡皮筋。 橡皮筋使得視圖不想大于其尺寸中的內(nèi)在內(nèi)容大小。 每個優(yōu)先級與0到1000之間的值相關(guān)聯(lián)。值為1000表示視圖不能比該維度上的內(nèi)在內(nèi)容大小更大。
讓我們來看一個只有水平維度的例子。 假設(shè)您有兩個標(biāo)簽彼此相鄰,兩個視圖之間以及每個視圖及其父級視圖之間的約束如圖13.4所示。
圖13.4 兩個標(biāo)簽并排
在父視圖變得越來越大之前這樣顯示看起來還很不錯。 但到了那時候哪個標(biāo)簽應(yīng)該變得更寬? 第一個標(biāo)簽,第二個標(biāo)簽,還是兩者? 如圖13.5所示,界面目前是模糊的。
圖13.5 模糊的布局
這就是 content hugging priority
。 具有較高 content hugging priority
的視圖是不拉伸的視圖。 你可以把優(yōu)先值看作是橡皮筋的 “強(qiáng)度”。 優(yōu)先級越高,橡皮筋越堅固,它想要擠壓它的內(nèi)在內(nèi)容的尺寸就越大。
注:也就是優(yōu)該值越大,越難拉伸
Content compression resistance priorities
Content compression resistance priorities
決定了一個視圖拒絕小于其固有內(nèi)容大小的程度。 請考慮圖13.4中的相同的兩個標(biāo)簽。 如果父級視圖的寬度減小了會發(fā)生什么? 其中一個標(biāo)簽需要截斷它的文本(圖13.6)。 但是是哪一個呢?
圖13.6 被壓縮的模糊布局
具有較高 Content compression resistance priorities
的視圖是能夠抵抗壓縮的視圖,因此不會截斷其文本。
有了這些知識,您現(xiàn)在就可以用棧視圖來解決問題了。
選擇 Date Created
標(biāo)簽并打開其大小(Size)檢查器。 找到 Vertical Content Hugging Priority
并將其降低到249.現(xiàn)在其他三個標(biāo)簽擁有更高的 content hugging priority
,所以他們將擠壓其內(nèi)在的內(nèi)容高度。 Date Created
標(biāo)簽將伸展以填充剩余空間。
棧視圖 distribution
我們來看看另一種解決問題的方法。 堆棧視圖具有可確定其內(nèi)容布局方式的多個屬性。
在畫布上或使用文檔大綱選擇棧視圖。 打開屬性檢查器,找到頂部標(biāo)記為 Stack View
的部分。 決定內(nèi)容如何布局的屬性之一是 Distribution
屬性。 目前它被設(shè)置為 Fill
,它允許視圖根據(jù)其固有的內(nèi)容大小來排列其內(nèi)容。 將值更改為 Fill Equally
。 這將調(diào)整標(biāo)簽的大小,使它們都具有相同的高度,忽略內(nèi)在的內(nèi)容大小(圖13.7)。 要想知道棧視圖可以具有的其他的 distribution
值,請閱讀文檔。
圖13.7堆棧視圖設(shè)置為fill equally
將堆棧視圖的 Distribution
更改為 Fill
; 這是你在這一章中想要的。
嵌套堆棧視圖
棧視圖最強(qiáng)大的功能之一是它們可以彼此嵌套。 您將使用此方式在較大的垂直棧視圖中嵌套水平棧視圖。 前三個標(biāo)簽將在其旁邊顯示一個文本字段,顯示該 Item 的相應(yīng)值,并允許用戶編輯該值。
在畫布上選擇 Name
標(biāo)簽。 單擊自動布局約束菜單中左側(cè)的第二個圖標(biāo):
這將將所選視圖嵌入到棧視圖中。
選擇新的棧視圖并打開其屬性檢查器。 棧視圖當(dāng)前是一個垂直棧視圖,但您希望它是一個水平棧視圖。 將 Axis
更改為 Horizontal
。
現(xiàn)在將 Text Field
從對象庫拖到 Name
標(biāo)簽的右側(cè)。 因為默認(rèn)情況下,標(biāo)簽擁有比文本字段更大的 content hugging priority
,所以標(biāo)簽會擠壓內(nèi)在的內(nèi)容寬度并且文本字段會延伸。 標(biāo)簽和文本字段目前具有相同的內(nèi)容壓縮阻力優(yōu)先級,如果文本字段的文本太長,這將導(dǎo)致模糊的布局。 打開文本字段的大小檢查器,并將其 Horizontal Content Compression Resistance Priority
設(shè)置為 749。這將確保文本字段的文本將在必要時被截斷,而不是標(biāo)簽。
棧視圖間隔
標(biāo)簽和文本框看起來有點(diǎn)小,因為它們之間沒有間隔。 棧視圖允許您自定義 item 之間的間隔。
選擇水平棧視圖并打開它的屬性檢查器。 改變 Spacing
為 8
。 注意,文本字段收縮以適應(yīng)間隔,因為它對壓縮的抵抗力比標(biāo)簽要小。
對 Serial
和 Value
標(biāo)簽重復(fù)以下步驟:
-
選擇標(biāo)簽,然后單擊
圖標(biāo)。
- 將堆棧視圖更改為水平(horizontal)棧視圖。
- 將
Text field
拖到水平棧視圖上,將其horizontal content compression resistance priority
改為749
。 - 更新堆棧視圖,
Spacing
為8
點(diǎn)。
你會想要對界面進(jìn)行另外的調(diào)整:垂直棧視圖需要一些間距。 Date Created
標(biāo)簽應(yīng)具有中心文本對齊方式。 Name
,Serial
和 Value
標(biāo)簽的寬度應(yīng)該相同。
選擇垂直棧視圖,打開其屬性檢查器,并將 Spacing
更新為 8
點(diǎn)。 然后選擇 Date Created
的標(biāo)簽,打開其屬性檢查器,并將 Alignment
更改為居中。 這解決了前兩個問題。
雖然棧視圖大大減少了您需要添加到界面中的約束數(shù)量,但有一些約束仍然很重要。 在界面上,由于標(biāo)簽寬度的差異,文本字段的前部不對齊。 (英文中的區(qū)別不是非常明顯,但當(dāng)本地化為其他語言時,這種差異變得更加顯著)為了解決這個問題,您將在三個文本字段之間添加前部約束。
右鍵選中 Name
文本字段拖動到 Seria
文本字段,并選擇 Leading
。 然后對 Serial
文本字段和 Value
文本字段執(zhí)行相同操作。 完成的界面如圖13.8所示。
圖13.8最終棧視圖界面
棧視圖會使你在短時間內(nèi)就能創(chuàng)建非常豐富的界面,比您在使用手動配置約束需要的時間要短。 約束仍然要被添加,但是它們是由堆棧視圖本身去管理,而不是你。 棧視圖允許您在運(yùn)行時具有非常動態(tài)的界面。 您可以通過使用 addArrangedSubview(_ :),insertArrangedSubview(_:at :) 和 removeArrangedSubview(_ :) 來添加和刪除視圖。 您還可以在棧視圖中的視圖上切換 hidden
屬性。 堆棧視圖將通過該值自動布局其內(nèi)容。
Segues
大多數(shù)iOS應(yīng)用程序都有許多視圖控制器,用戶可以在其中進(jìn)行導(dǎo)航。故事板允許您將這些交互設(shè)置為 segues
,而無需編寫代碼。
一個 segue
能將另一個視圖控制器的視圖移到屏幕上,并由 UIStoryboardSegue 的一個實例來表示。 每個 segue
都有一個 樣式(style
)、一個 動作項(action item
) 和一個 標(biāo)識符(identifier
)。 segue
的樣式?jīng)Q定了視圖控制器的呈現(xiàn)方式。 動作項是在故事板文件中觸發(fā) segue
的視圖對象,如按鈕、表視圖單元或其他 UIControl。 標(biāo)識符用于以編程方式訪問 segue
。 當(dāng)您想要觸發(fā)一個不是來自某個動作項的 segue
時,這是很有用的,比如抖動或其他無法在故事板文件中設(shè)置的界面元素。
讓我們從一個 show
segue
開始。 show segue
顯示一個視圖控制器,這取決于它顯示的上下文。 segue
將在表視圖控制器和新視圖控制器之間。 動作項將是表視圖的 cell; 點(diǎn)擊一個 cell 將會顯示視圖控制器。
在 Main.storyboard
中,在 Items View Controller
上選擇 ItemCell 的 prototype cell。 右鍵從 cell 拖動到上一節(jié)中新設(shè)置的視圖控制器。 (確保您是從 cell 拖動而不是表視圖!)接著將出現(xiàn)一個黑色面板,其中列出了可能的樣式。 從 Selection Segue
部分中選擇 Show
(圖13.9)。
圖13.9 設(shè)置一個 show segue
注意從表視圖控制器到新視圖控制器的箭頭。 這是一個 segue
。 圓圈中的圖標(biāo)告訴你這個 segue
是一個 show segue
,每個 segue
都有一個唯一的圖標(biāo)。
構(gòu)建并運(yùn)行應(yīng)用程序。 點(diǎn)擊 cell,新的視圖控制器將從屏幕底部向上滑動。 (從底部滑出是以模態(tài)方式呈現(xiàn)視圖控制器時的默認(rèn)行為。)
到現(xiàn)在為止還挺好! 但目前有兩個問題:視圖控制器不顯示所選 Item 的信息,并且無法關(guān)閉視圖控制器以返回到 ItemsViewController。 您將在下一節(jié)中解決第一個問題,在第14章中解決第二個問題。
掛鉤內(nèi)容
要顯示所選 Item 的信息,您將需要創(chuàng)建一個新的 UIViewController 子類。
創(chuàng)建一個新的 Swift文件 并將其命名為 DetailViewController。 打開 DetailViewController.swift
并聲明一個名為 DetailViewController 的新 UIViewController 子類。
import Foundation
import UIKit
class DetailViewController: UIViewController {
}
因為您需要能夠訪問您在運(yùn)行時創(chuàng)建的子視圖,DetailViewController 需要它們的 outlet。 該計劃是向 DetailViewController 添加四個新 outlet,然后進(jìn)行連接。 在以前的練習(xí)中,您已經(jīng)做了兩個不同的步驟:首先,您在 Swift 文件中添加 outlet,然后在故事板文件中進(jìn)行了連接。 其實您可以使用助理編輯器一次完成。
打開 DetailViewController.swift
,在項目導(dǎo)航器中選擇 Main.stroyboard
。 這將打開在 DetailViewController.swift
旁邊的助理編輯器中的文件。 (您可以通過單擊工作區(qū)頂部的 Editor
控件中的中間按鈕來切換助理編輯器,顯示助理編輯器的快捷方式為 Command-Option-Return
,返回標(biāo)準(zhǔn)編輯器的快捷方式為 Command-Return
。)
你的窗口已經(jīng)變得有些凌亂。 讓我們做一些暫時的修改。 通過單擊工作區(qū)頂部的 View
控件中的左側(cè)按鈕隱藏導(dǎo)航區(qū) (此快捷方式為 Command-0
)。 然后,通過單擊編輯器左下角的切換按鈕(右上角左邊那三個按鈕的中間的那個顯示像兩個圓交叉的那個按鈕),在 Interface Builder
中隱藏文檔輪廓。 您的工作區(qū)現(xiàn)在應(yīng)如圖13.10所示。
圖13.10 布局工作區(qū)
在連接 outlet 之前,您需要告知詳細(xì)界面應(yīng)該與 DetailViewController 相關(guān)聯(lián)。 在畫布上選擇 View Controller
并打開其身份檢查器。 將 Class
更改為 DetailViewController
(圖13.11)。
圖13.11 設(shè)置視圖控制器類
UITextField 的三個實例和 UILabel 的底部實例將在 DetailViewController 中顯示。 右鍵選中從 Name
標(biāo)簽旁邊的 UITextField 拖動到 DetailViewController.swift
的頂部,如圖13.12所示。
圖13.12 從故事板拖動到源文件
放手后會出現(xiàn)彈出窗口。 在 Name
字段中輸入 nameField
,確保 Storage
設(shè)置為 Strong
,然后單擊 Connect
(圖13.13)。
圖13.13 自動生成outlet并進(jìn)行連接
這將在 DetailViewController 中創(chuàng)建一個名為 nameField
的類型為 UITextField 的 @IBOutlet
屬性。
此外,這個 UITextField 已經(jīng)連接到 DetailViewController 的 nameField
outlet。 您可以通過單擊 Detail View Controller
查看連接來驗證這一點(diǎn)。 還要注意,將鼠標(biāo)懸停在面板上的 nameField
連接之上,將會顯示您連接的 UITextField。
以相同的方式創(chuàng)建其他三個 outlet,并將其命名,如圖13.14所示。
圖13.14連接圖
進(jìn)行連接后,DetailViewController.swift
應(yīng)該如下所示:
import UIKit
class DetailViewController: UIViewController {
??@IBOutlet var nameField: UITextField!
??@IBOutlet var serialNumberField: UITextField!
??@IBOutlet var valueField: UITextField!
??@IBOutlet var dateLabel: UILabel!
}
如果您的文件看起來不一樣,那表明您的 outlet 連接不正確。 解決您的文件與上述代碼之間的任何差異的三個步驟:首先,通過 右鍵——拖動 流程并再次進(jìn)行連接,直到您在 DetailViewController.swift
中顯示上述四行。 第二,刪除創(chuàng)建的任何錯誤代碼(如非屬性方法聲明或?qū)傩裕?最后,檢查故事板文件中的任何不良連接——在 Main.storyboard
中,右擊 Detail View Controller*。 如果任何連接旁邊都有黃色警告標(biāo)志,請點(diǎn)擊這些連接旁邊的
x` 圖標(biāo)以將其斷開連接。
確保界面文件中沒有錯誤的連接很重要。 當(dāng)更改屬性名稱但不更新界面文件中的連接或完全刪除屬性但不從界面文件中刪除時,通常會發(fā)生連接錯誤。 無論哪種方式,連接錯誤都會導(dǎo)致應(yīng)用程序在加載界面文件時崩潰。
連接完成后,您可以關(guān)閉助理編輯器,并返回到僅查看 DetailViewController.swift
。
DetailViewController 將持有對正在顯示的 Item 的引用。 加載視圖后,您將會將每個文本字段上的文本從 Item 實例中設(shè)置為適當(dāng)?shù)闹怠?/p>
在 DetailViewController.swift
中,為 Item 實例添加一個屬性,并重寫 viewWillAppear(_ :) 來設(shè)置界面。
class DetailViewController: UIViewController {
??@IBOutlet var nameField: UITextField!
??@IBOutlet var serialNumberField: UITextField!
??@IBOutlet var valueField: UITextField!
??@IBOutlet var dateLabel: UILabel!
??var item: Item!
??override func viewWillAppear(_ animated: Bool) {
????super.viewWillAppear(animated)
????nameField.text = item.name
????serialNumberField.text = item.serialNumber
????valueField.text = "\(item.valueInDollars)"
????dateLabel.text = "\(item.dateCreated)"
??}
}
這是有用的,我們最好使用格式化器而不是使用字符串插值來打印出 valueInDollars
和 dateCreated
。 您在第 4 章中使用了 NumberFormatter 的實例。您將在此使用另一個實例,以及 DateFormatter 的實例來格式化 dateCreated
。
將 NumberFormatter 和 DateFormatter 的實例添加到 DetailViewController。 在 viewWillAppear(_ :) 中使用這些格式化器來格式化 valueInDollars
和 dateCreated
。
var item: Item!
let numberFormatter: NumberFormatter = {
??let formatter = NumberFormatter()formatter.numberStyle = .decimal
??formatter.minimumFractionDigits = 2
??formatter.maximumFractionDigits = 2
??return formatter
}()
let dateFormatter: DateFormatter = {
??let formatter = DateFormatter()
??formatter.dateStyle = .medium
??formatter.timeStyle = .none
??return formatter
}()
override func viewWillAppear(_ animated: Bool) {
??super.viewWillAppear(animated)
??nameField.text = item.name
??serialNumberField.text = item.serialNumber
??valueField.text = "\(item.valueInDollars)"
??dateLabel.text = "\(item.dateCreated)"
??valueField.text = numberFormatter.string(from: NSNumber(value: item.valueInDollars))
??dateLabel.text = dateFormatter.string(from: item.dateCreated)
}
傳遞數(shù)據(jù)
當(dāng)表視圖中的一行被點(diǎn)擊時,您需要一個告訴 DetailViewController 當(dāng)前選擇了哪個 Item 的方法。 每當(dāng)觸發(fā) segue 時,都會在啟動 segue 的視圖控制器上調(diào)用 prepare(for:sender :) 方法。 該方法有兩個參數(shù):UIStoryboardSegue——它提供有關(guān)哪個 segue 正在發(fā)生的信息,以及 發(fā)送者(sender
)——它是觸發(fā) segue 的對象(例如 UITableViewCell 或 UIButton)。
UIStoryboardSegue 為您提供了三條信息:源視圖控制器(segue 起始位置),目標(biāo)視圖控制器(segue 結(jié)束位置)以及 segue 的標(biāo)識符。 標(biāo)識符可以區(qū)分分隔符。 讓我們給 segue 一個有用的標(biāo)識符。
再次打開 Main.storyboard
。 通過單擊兩個視圖控制器之間的箭頭并打開屬性檢查器來選擇 show segue。 對于 identifier
,輸入 showItem
(圖13.15)。
圖13.15 Segue 標(biāo)識符
隨著學(xué)習(xí)的深入,你現(xiàn)在可以傳遞你的 Item 實例。 打開 ItemsViewController.swift
并實現(xiàn) prepare(for:sender :)。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
??// If the triggered segue is the "showItem" segue
??switch segue.identifier {
??case "showItem"?:
????// Figure out which row was just tapped
????if let row = tableView.indexPathForSelectedRow?.row {
??????// Get the item associated with this row and pass it along
??????let item = itemStore.allItems[row]
??????let detailViewController = segue.destination as! DetailViewController
??????detailViewController.item = item
????}
??default:
????preconditionFailure("Unexpected segue identifier.")
??}
}
您在第2章中了解了 switch
語句。這里,您正在使用它來切換可能的 segue 標(biāo)識符。 因為 segue 的 identifier
是一個可選的String, 因為其后面加上了?。 默認(rèn)塊使用 preconditionFailure(_ :) 函數(shù)捕獲任何意外的 segue 標(biāo)識符并使應(yīng)用程序崩潰。 如果程序員忘記給出一個標(biāo)識符,或者如果在某個地方出現(xiàn)了一個帶有 segue 標(biāo)識符的錯字,那就會出現(xiàn)這種情況。 任何一種情況都是程序員的錯誤,使用 preconditionFailure(_ :) 可以幫助您更快地識別這些問題。
構(gòu)建并運(yùn)行應(yīng)用程序。 點(diǎn)擊一行,DetailViewController 將在屏幕上滑動,顯示該 item 的詳細(xì)信息。 (您將在第14章中修復(fù)無法返回 ItemsViewController 的問題)
許多新加入iOS的程序員都在與視圖控制器之間如何傳遞數(shù)據(jù)進(jìn)行斗爭。 將根視圖控制器中的所有數(shù)據(jù)并將數(shù)據(jù)的子集傳遞給下一個 UIViewController (就像本章所做的那樣) 是一種簡潔高效的方法。
青銅挑戰(zhàn):更多棧視圖
棧視圖對 Quiz
和 WorldTrotter
也很好用。 使用 UIStackView 更新這兩個應(yīng)用程序。