一、前言
Swift版本 4.0
Xcode版本 9.2
這周本來我是想要寫其他知識的,但在構建 Demo
工程的時候, 我情不自禁的就使用了 Storyboard
(下面簡稱 SB
),或者說是 Interface Builder
(下面簡稱 IB
),所以就想著寫一篇相關文章。
這裡不討論使用這種方式的好壞,大家仁者見仁,智者見智,貓神的文章連結在後記裡面,我的觀點和他一致。
二、Storyboard基礎
這部分針對完全沒用過
SB
的讀者,極其基礎,熟悉的直接跳過!
2.1 完成目標的概覽
下面是我這一小節需要完成目標的樣子,一個遊戲的展示介面和新增遊戲。

2.2 介面初識
先建立一個 Demo
專案,點選 Main.storyboard
出現如下介面:

首先需要了解 SB
中幾個重要的區域,這裡是按照我的理解取的名字,只是簡單說明區域的作用,後面會詳細使用這幾個區域,如上圖所示:
- 1、選單導航區域,新增的控制器、控制器之間的跳轉
Segue
及控制器上面的控制元件和佈局等等資訊都在這裡顯示。 - 2、工作展示區域: 可以在這裡給各個控制器新增控制元件和預覽佈局後的控制元件。
- 3、配置區域: 可以在這裡將
SB
和程式碼檔案關聯和檢視關聯後的資訊,也可以直接在這裡配置控制元件的屬性等等。 - 4、佈局區域: 上面面有很多機型選擇,可以直接選擇機型和方向,區域2會根據選擇的機型和自動佈局直接預覽控制元件顯示的效果和佈局,中間的加減符號可以放大和縮小區域2中的內容,右上角的幾個按鈕可以進行自動佈局操作。
- 5、控制元件區域,我們可以直接在這裡選擇控制元件,然後拖入區域2中。
細心的讀者可能會發現,區域1中,控制器在一個 scene
的下面,在 SB
中,scene
就對應著一個控制器。區域2裡面還有一個灰色箭頭,它代表這個控制器是當前 SB
檔案的入口,會在後面詳細的講解。
2.3 新增控制元件

直接從控制元件區域拖拽了一個 UIView
控制元件到控制器上,然後在 Attributes inspector區域
(點選配置區域中那個楔子形狀的按鈕)直接配置背景顏色為灰色。如果顯示選單欄中沒有沒有你想要的顏色,點選 other
,裡面有多種方式配置顏色,如 RGB
和16進位制顏色等等。
上圖這個區域裡還有一些其他的屬性可以配置,例如 UILabel
控制元件字型和字型顏色等等屬性等,就不深入去展開了。
讀者肯定注意到,控制元件在拖拽中,控制器出現了輔助虛線,可以提醒你相對其他檢視的位置資訊,圖中所示的其中一條就是父檢視的中線。
2.4 佈局控制元件

解釋一下上圖的自動佈局操作:
- 1.選中控制元件, 點選
Align
按鈕,勾選Horizontalliy in Container
和Vertically in Container
,然後新增這兩個佈局,相對於父檢視水平和垂直居中 - 2.然後點選
Add New Constraints
按鈕,新增Width
和Height
約束,都為200。
到這裡佈局就完成了,因為大小和位置都已經確定。觀察上圖,我只新增了 Align
約束時,介面出現了紅線,這代表約束不完整。並且選單欄上方出現帶有箭頭的小紅點,可以點選進去檢視還有哪些約束沒有完成。這裡還有其他的約束選項,讀者可以自行嘗試。
- 3、圖中最後,我在
Size inspector
區域 (點選配置區域中那個小直尺按鈕) 雙擊寬度約束,進入了詳情配置介面,這裡可以對約束進行二次修改。點選選單欄的約束,同樣可以進入這個介面。
這裡還有另一種方式進行自動佈局,如圖所示:

按住 Ctrl
,然後選中灰色 View
,移動滑鼠會出現一條線,拖到你想要相對其佈局的控制元件,圖中選擇的是父檢視,出現了一個選單讓你選擇約束條件。同樣的操作也能直接在選單欄中進行。甚至當控制器上控制元件比較多不容易選中時,可以直接從控制器上拖到選單欄上的控制元件上。
這一部分的操作是很簡單的,不過需要自動佈局的相關知識。
2.5 開始一個TableView介面
選中選單欄的 View Controller Scene
,然後點選鍵盤上的 delete
鍵,刪除我們鼓搗的控制器。
從控制元件區域拖拽一個 UITabBarController
到工作展示區域:
在工作展示空白區域,雙擊滑鼠左鍵和單點滑鼠右鍵,可以放大,縮小顯示內容。

如圖,UITabBarController
(它和 UINavigationController
都是容器控制器) 會自帶兩個子控制器,並且有兩個箭頭從 TabBarController
指向它們,這個箭頭的術語叫做 Segue
, 這裡的是 Relationship Segue
,代表控制器之間的關係。
刪掉 item1
的控制器,拖拽一個 UITableViewController
出來,然後讓它成為 UINavigationController
的子控制器, 再讓 UINavigationController
成為 UITabBarController
控制器的子控制器,操作如圖所示:

當然你也可以直接拖拽一個 UINavigationController
出來,然後按住 Control
拖動選擇 view controllers
。不過我覺得點選 Editor
這種方式更加便捷。
讓我們的關注點來到 UITableViewController
,看到介面上有一個 Prototype Cells
,可以理解為我們平時使用的那種 Cell
, 與之對應的是 Static Cells
, 從名字就可以看出來,這種是靜態的,不能夠迴圈使用,並且只能在UITableViewController
上使用。

紅框中,和我們程式碼實現中官方提供的4種 Cell
一樣,不過這裡我們需要自定義,下面是完成後的樣子。

可能對沒有接觸過 IB
的讀者來說,這裡還是比較麻煩,所以詳細描述一下。
選中 Cell
在右上角 Size inspector
區域修改 Cell
的高度為120,這裡的高度設定只是方便我們進行佈局,並不是實際顯示的高度。

拖動一個 UIImageView
控制元件到 Cell
裡面,進行佈局。
iOS8
以後更新了讓Cell
自己自適應高度的新特性,所以這裡我們不光要確定自己的位置和大小,還需要將自己的大小反饋給Cell
讓其自適應高度,後面詳細使用。
相對於父檢視:
距離右邊20,距離上邊10,寬高100,這就已經確定了位置和大小,不過為了讓 Cell
知道我們的高度,還需要設定一個距離底部的距離。這樣 Cell
就知道顯示的時候需要的高度。結合我們目標的樣子,底部距離的設定是有個小問題的,後面來糾正。


繼續拖動一個 UILable
到 Cell
裡。
相對於父檢視: 距離左邊15,上邊10
相對於 UIImageView
: 距離它的左邊10


然後比較麻煩的地方來了。再拖動一個 UILable
到 Cell
裡。
相對於父檢視: 距離左邊15,距離底部10。
相對於 Game Name Label
:距離其底部10。
相對於 UIImageView
: 距離它的左邊10。
按照邏輯來說沒問題呀,因為上下左右都給了約束,是什麼原因呢?


我們點選紅框中的小紅點進行檢視:

UILabel
和UIButton
等控制元件有一個特點,它會根據內容自適應自己的大小。
如圖所示兩個 Label
在反饋大小給 Cell
時,Cell
也同樣會反饋自己的大小給兩個 Label
,這就會產生兩個問題:
-
1.如果
Cell
高度比內容反饋需要的高度大的時候,需要拉伸哪個部分的內容? -
2.如果
Cell
高度比內容反饋需要的高度小的時候,需要壓縮哪個部分內容?

這裡就需要談到 AutoLayout
中的 Content Hugging
和 Content Compression Resistance
。
- 1.
Content Hugging Priority
: 對應上面的第1中情況,這個屬性的值越高,就越不容易被拉伸。 - 2.
Content Compression Resistance
:對應上面的第2種情況,這個屬性的值越高,就越不容易被壓縮。
顯然上面報錯的原因是 Cell
的高度比兩個 Label
的內容高度大了,屬於第一種情況,我們讓 Game Name Label
不拉伸, 增加它的 Content Hugging Priority
(預設值為251)比另一個 Label
大(增加到252)。

這個問題解決了,但新問題又出現了:

因為 Game Detail Label
被拉伸,導致了內容居中,這看上去怪怪的,以前看到有關於討論讓 Label
居上的問題。但這並不是這裡的解決辦法。還記得前面說過可以對約束進行二次編輯嗎?選中 Game Detail Label
的 Bottom
約束,可以在選單區域選擇或者在小直尺圖示區域裡面找到它進行雙擊,就來到如下介面:

這裡有個 Relation
選項,點看可以看到:

沒錯我們選擇讓這個約束大於或等於10:

看上去是完成了,回到最初新增 UIImageView
約束的時候,我說過有一個小問題,UIImageView
的約束強行的讓 Cell
的高度為120了。當 Label
內容很多換行超過120的時候,就會出現上面的第2種情況, Cell
高度不夠完整顯示內容,這顯然不是我們想要的結果。所以修改 UIImageView
的 Bottom
約束也為距離底部大於等於10,到這裡佈局就結束了,最後別忘了設定 identifier
:

為了讓 SB
和程式碼關聯起來,建立一個繼承自 UITableController
的 GameVC.swift
檔案、繼承自 UITableViewCell
的 GameCell.swift
檔案和資料模型 Game.swift
檔案,然後依次選中 SB
中的檔案關聯:


繼續將 SB
中的屬性和程式碼關聯起來:

也可以直接從控制器中選中控制元件並按住 Control
進行拖動連線,這裡就不再舉例了,這裡不僅僅只能屬性連線,例如 UIButton
可以直接連線一個點選響應方法等等。連線後可以在 Connections inspectors
(圓圈包含一個箭頭的按鈕) 檢視:

注意: 一個控制元件屬性關聯多次或者其他關聯錯誤會引發執行奔潰,這是新手最容易犯的問題,如果名字寫錯了,需要先取消上次的關聯,再重新關聯。
Game.swift
檔案中:
struct Game {
let name: String
let detail: String
let pictureName: String
static func getData() -> [Game] {
return [
Game(name: "絕地求生",
detail: "神仙打架遊戲",
pictureName: "game_one"),
Game(name: "英雄聯盟",
detail: "《英雄聯盟》(簡稱LOL)是由美國拳頭遊戲(Riot Games)開發、中國大陸地區騰訊遊戲代理運營的英雄對戰MOBA競技網遊。遊戲裡擁有數百個個性英雄,並擁有排位系統、符文系統等特色養成系統。《英雄聯盟》還致力於推動全球電子競技的發展,除了聯動各賽區發展職業聯賽、打造電競體系之外,每年還會舉辦“季中冠軍賽”“全球總決賽”“All Star全明星賽”三大世界級賽事,獲得了億萬玩家的喜愛,形成了自己獨有的電子競技文化",
pictureName: "game_two")]
}
}
複製程式碼
GameVC.swift
檔案中:
class GameVC: UITableViewController {
var games: [Game] = []
override func viewDidLoad() {
super.viewDidLoad()
games = Game.getData()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return games.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "GameCell", for: indexPath) as! GameCell
cell.game = games[indexPath.row]
return cell
}
}
複製程式碼
GameCell.swift
檔案中:
class GameCell: UITableViewCell {
var game: Game! {
didSet {
gameNameLabel.text = game.name
gameDetailLabel.text = game.detail
gameImageView.image = UIImage(named: game.pictureName)
}
}
@IBOutlet weak var gameImageView: UIImageView!
@IBOutlet weak var gameNameLabel: UILabel!
@IBOutlet weak var gameDetailLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
}
複製程式碼
準備工作完畢,執行 Demo
:

出錯了,細心的讀者肯定早就發現這個問題了,那就是前面說的那個入口箭頭:

配置完畢後再執行:

2.6 介面之間跳轉
先給控制器增加一個 title
:

然後把 Game
控制器拖到 UITabBarController
第一個位置:

繼續新增一個 UIBarButtonItem
, 並設定風格為 Add
:

新增一個 UITableViewController
並讓其成為 UINavigationController
的子控制器 ,按住Control
點選 Add Item
,拖動到新的控制器上,會出現彈窗選擇跳轉方式,這裡選擇 Present Modally
,對應著我們程式碼中 Present
那個方法。
這裡的 Show
代表,如果是 UINavigationController
的子控制器就會執行 Push
方法,不是就會執行 Present
方法。

兩個控制器之間多出了一個帶箭頭的連線,這可以理解為介面切換 Segue
,用來描述控制器之間的跳轉,一個介面切換 Segue
只能單向跳轉。
設定新控制器的 title
為 Game Add
,左邊新增一個 Cancle Item
, 右邊新增一個 Done Item
。然後繼續在 GameVC.Swift
的底部新增分類。
// MARK: - IBActions
extension GameVC {
@IBAction func cancelToGameVC(_ segue: UIStoryboardSegue) {
}
@IBAction func saveGameDetail(_ segue: UIStoryboardSegue) {
}
}
複製程式碼
這是 unwind Segue
,用來返回到目標控制器。直接上圖:

選中 Game Add
中的 TableView
.接下來我直接用 static cell
進行佈局,
- 1.
content
中選擇Static Cells
,Style
中選擇Grouped
。 - 2.將出現的
Section
中的Cell
刪除到只剩一個,設定Cell
的Selection
為None
, 直接複製Section
,這樣就有兩個含有一個Cell
的Section
。 - 3.給
Section
設定標題(SB
中的Header
)為Game Name
和Game Detail
。 - 4.將第一個
Section
高度設為50,第二個設為200。拖一個UITextField
到第一個Cell
,佈局上0底0左10右10,拖一個UITextView
到第二個Cell
,佈局上底左右都是10。 - 5.建立繼承於
UITableViewController
的GameAddVC.swift
檔案,然後將裡面方法刪除到只剩viewDidLoad
, 並關聯這個SB
。 - 6.將步驟3中的
UITextField
和UITextView
連線到GameAddVC.swift
檔案中生成@IBOutlet
屬性。
@IBOutlet weak var gameNameTextField: UITextField!
@IBOutlet weak var gameDetailTextView: UITextView!
複製程式碼
這裡之所以能直接將
Cell
中的屬性直接連線到控制器中,是因為靜態Cell
不會重用。
配置完成如下:

這裡省略了新增圖片的步驟,直接設定一個預設圖片。
-
選中剛才
Done Item
新增的Segue
,然後設定它的Identifier
為AddGameDetail
。 -
在
AddGameVC.swift
中重寫父類方法並新增程式碼:
var game: Game?
// 這個方法點選 `Done` 的時候會呼叫
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "GameAddDetail",
let name = gameNameTextField.text {
game = Game(name: name,
detail: gameDetailTextView.text!,
pictureName: "game_default")
}
}
複製程式碼
- 在
GameVC.swift
中,新增如下程式碼:
// 這個方法前面有,只是新增方法內的內容
@IBAction func saveGameDetail(_ segue: UIStoryboardSegue) {
guard let gameAddVC = segue.source as? GameAddVC,
let game = gameAddVC.game else {
return
}
games.append(game)
let indexPath = IndexPath(row: games.count - 1, section: 0)
tableView.insertRows(at: [indexPath], with: .automatic)
}
複製程式碼
執行 Demo
, 如下:

2.7 Storyboard Reference
在 SB
中,如果多個人同時對一個地方(例如同一個控制器)進行修改,很容易造成 Git
衝突,這也是反對者們反對使用 SB
的一個理由。不過在蘋果增加 Storyboard Reference
功能後,這種情況在開發中完全可以避免了。

Demo
中的控制器數量較少,但在實際專案中,如果多個人都都只操作這個 Main.storyboard
,那將是一件很恐怖的事情。之前沒有 Storyboard Reference
功能時,多個 SB
之間的跳轉只能使用程式碼的方式實現。現在來看看 Storyboard Reference
吧。
- 1.按住滑鼠左鍵,然後圈中你想要脫離
Main.storyboard
的控制器,就像桌面用滑鼠多選檔案那樣。 - 2.點選 Editor>Refactor to Storyboard。
- 3.取名為
Game.storyboard
,選擇在哪個資料夾下面建立,然後確定。

完成後,我們可以看到 Main.storyboard
中的控制器變成了一個了 Storyboard Reference
,其他控制器移到我們新建立的 Game.storyboard
中去了。多人開發時,各自操作自己的業務 SB
,就基本避免了 Git
衝突。
同樣,我們也可以先直接建立 SB
檔案,然後再從控制元件區拖拽一個 Storyboard Reference
, 然後再讓它和我們新建立的 SB
檔案關聯。

到這裡這一下節就結束了,我自認為是寫得比較囉嗦,不過這也是沒有辦法的選擇,這部分知識更多的是介面上的操作,如果不寫明白,不容易闡述清楚!
3 Storyboard高階用法
這裡的所謂高階用法,是我一廂情願認為的。
3.1 @IBInspectable
如果你之前沒有見過這個東西,那麼你肯定為某些屬性沒有暴露在 IB
的設定皮膚中而困擾過。@IBInspectable
的用處很簡單,就是讓我們自定義的屬性也能直接在 IB
中選擇,例如貓神的文章中的建議:
- 為一個顯示文字的 view 設定本地化字串:
extension UILabel {
@IBInspectable var localizedKey: String? {
set {
guard let newValue = newValue else { return }
text = NSLocalizedString(newValue, comment: "")
}
get { return text }
}
}
extension UIButton {
@IBInspectable var localizedKey: String? {
set {
guard let newValue = newValue else { return }
setTitle(NSLocalizedString(newValue, comment: ""), for: .normal)
}
get { return titleLabel?.text }
}
}
extension UITextField {
@IBInspectable var localizedKey: String? {
set {
guard let newValue = newValue else { return }
placeholder = NSLocalizedString(newValue, comment: "")
}
get { return placeholder }
}
}
複製程式碼
IB
中可以直接設定:

- 為一個
image view
設定圓角(這裡可以直接擴充套件UIView
)
@IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
複製程式碼
IB 中可以直接設定:

僅僅使用 @IBInspectable
無法將屬性的設定實時顯示出來,還需要另一個關鍵字的幫助。
3.2 @IBDesignable
它能夠將一些繪圖程式碼和 UIView
及其子類的 @IBInspectable
屬性實時渲染到 IB
中。
- 1.結合
@IBInspectable
使用,建立UIView
子類CustomView
。拖拽一個UIView
到另一個Item
控制器上,佈局上下居中,款高200,然後將它們關聯。此時如圖所示:

- 2.在
CustomView.swift
中新增程式碼,注意@IBDesignable
的位置:
@IBDesignable
class CustomView: UIView {
@IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
@IBInspectable var borderColor: UIColor = UIColor.white {
didSet {
layer.borderColor = borderColor.cgColor
}
}
@IBInspectable var borderWidth: Int = 1 {
didSet {
layer.borderWidth = CGFloat(borderWidth)
}
}
}
複製程式碼
然後看效果:

- 3.再新增繪圖程式碼,並將上面的
Corner Radius
設為0:
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: rect)
UIColor.green.setFill()
path.fill()
}
複製程式碼
結果如圖:

3.3 自定義Segue跳轉動畫
我們都知道 Presnet
切換時系統預設的公開動畫有四種,如果我們想自定義的話,需要建立一個 UIStoryboardSegue
的子類。

class CustomAnimationPresentationSegue: UIStoryboardSegue, , UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {
override func perform() {
destination.transitioningDelegate = self
super.perform()
}
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
if transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) == destination {
// Presenting.
UIView.performWithoutAnimation {
toView.alpha = 0
containerView.addSubview(toView)
}
let transitionContextDuration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: transitionContextDuration, animations: {
toView.alpha = 1
}, completion: { success in
transitionContext.completeTransition(success)
})
}
else {
// Dismissing.
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
UIView.performWithoutAnimation {
containerView.insertSubview(toView, belowSubview: fromView)
}
let transitionContextDuration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: transitionContextDuration, animations: {
fromView.alpha = 0
}, completion: { success in
transitionContext.completeTransition(success)
})
}
}
}
複製程式碼
自定義了一個簡單的漸隱動畫,這裡關於自定義的跳轉動畫的部分我不想仔細探討(排在我想寫內容的佇列總)。然後我們在 IB
關聯跳轉到新增遊戲的 Segue
和 Cancle&Done
的 unwind Segue
為 CustomAnimationPresentationSegue
。
演示效果:

3.4 使用R.swift三方框架
其實這不算是 IB
的高階使用,它能夠掃描整個專案中的資原始檔(比如圖片名,View Controller
和 segue
的 identifier
等),並生成一種型別安全的獲取方式。
let icon = UIImage(named: "settings-icon")
let viewController = UIStoryboard(name: "Main",
bundle: nil).instantiateViewController(withIdentifier: "myViewController") as! MyViewController
複製程式碼
let icon = R.image.settingsIcon()
let viewController = R.storyboard.main.myViewController()
複製程式碼
四、後記及Demo
關於 IB
的操作目前我知道的就這些,如果你有更好的使用技巧可以評論分享討論一下。
最近我撿起了我的微博,因為很多 iOS
界的前輩都喜歡微博分享技術,我也關注了很多,收益匪淺。例如這個 OC
專案 ZHNCosmos Github地址,程式碼工整,邏輯清晰,我這個菜鳥準備好好學習一下。
另外附上我的微博,我每天都會轉發一些大佬的技術動態,請大家隨緣關注:
參考文章
WWDC2015視訊自帶中文字幕 What's New in Storyboards