不一樣於國外,StoryBoard
從面世到現在飽受國內開發者的質疑,質疑的理由不少,什麼不利於多人協做啊,隱藏了UI細節啊,出問題不容易測試,下降執行效率啊等等。此文就是針對這些問題的舉例和剖析。git
StoryBoard
和 Xib
有什麼區別?StoryBoard
和 Xib
都是用來分離UI樣式代碼,改善視圖代碼重用率,增長所見即所得,下降視圖測試繁複度的視圖系列化工具,swift
- 其中
Xib
以視圖View
爲主,StoryBoard
以控制器Controller
及其之間的關係,以及和視圖View
的關係爲主。
實際使用例子參見《純Swift項目-Xib | StoryBoard 設備適配技巧》或其餘StoryBoard
文章數組
StoryBoard
和 Xib
不利於多人協做,git
合併代碼容易衝突,且難以處理?這個是詆譭StoryBoard
最多的理由,也是看上去
最充分的理由。最顯著的就是下圖這種失敗的例子。緩存
在一個Storyboard
中,大量的Controller
控制器和Segue
連線彰顯着錯綜複雜的UI關係,令人望而生畏或者難以維護。app
Storyboard
的鍋,僅僅是使用者對工具的濫用!沒錯,就是濫用
,不管是Storyboard
也好,純代碼也罷,它們的本質都是工具,工具自己沒有正義或邪惡,影響工具的是使用者。哪怕是用純代碼開發,若是沒有命名規範,肆意的嵌套if
,不遵照MVC或者MVVM等開發模式,不區分開發環境與生產環境,這樣寫出來的代碼又何談可維護性,和多人協做呢?ide
Storyboard
纔不算濫用?避免濫用,最好的方法就是定製規範,就好像代碼中的諸多規範同樣。每一個團隊可能有本身不一樣的喜愛,我在此拋磚引玉,列出咱們團隊使用Storyboard
的規範,供你們參考。工具
每一個模塊獨立Storyboard | 每一個Storyboard只應該有一個主VC和同頁的子VC,主VC不該存在2個以上 |
---|---|
- 一個項目中,Storyboard不應是孤立存在的,應該像
MVP
模式那樣,每一個頁面都有獨立的Storyboard,每一個Storyboard只應該有一個主VC和同頁的子VC,主VC不該存在2個以上。(絕大多數狀況下,一個Storyboard上只應該有一個VC)- 頁面間的
Segue
連線應該使用Stroyboard Reference Scene
,UITabBarController
的子頁由於複雜度應該當成主VC處置- 視圖的初始樣式應儘可能在Storyboard上屬性面板中設置,非極特殊狀況,佈局也應在Storyboard上使用各類約束配合完成。這樣有利於視圖樣式和視圖代碼分離,有利於視圖代碼重用性和兼容性提升。
- 對於邏輯複雜的VC,應添加Object對象,並綁定相應的類來分離邏輯代碼。
- 對於圓角,背景色,陰影等
CALayer
的樣式,應該使用擴展或子類化實例的形式,使用@IBInspectable
屬性關鍵字,在Storyboard屬性面板中設定初始樣式。- 對於自定義視圖,應使用
@IBDesignable
關鍵字保障在在Storyboard上所見即所得!
使用以上原則,只要任務分工合理,基本上不存在多人同時修改同一個Storyboard
的狀況,就算配合失誤偶然發生,精簡的Storyboard其代碼量也不大,藉助文件比較工具很容易就能處理git衝突。佈局
說到底,臃腫的
Storyboard
和臃腫的ViewController
同樣,都是難以維護且容易git衝突的。惟一的解決方案就是有節制的使用工具。post
StoryBoard
和 Xib
隱藏了UI細節,且容易致使ViewController
臃腫?與其說StoryBoard
和 Xib
隱藏了UI細節,倒不如說蘋果是但願經過他們來引導開發者正確的使用 視圖 和 控制器 ,他們建立視圖實例的時候都是經過性能
required init?(coder aDecoder: NSCoder) {
}
複製代碼
構造方法建立視圖實例。全部初始樣式都是在屬性面板中設置的值,經過
func setValue(_ value: Any?, forUndefinedKey key: String) {
......
}
複製代碼
來賦值給視圖對應的屬性。
至於說致使
ViewController
臃腫,更是荒謬,StoryBoard
提供了多種方案來分離代碼,只不過不少人不知道而已。
拿美團的主頁UI舉例
這樣的首頁較爲複雜,正常佈局的話須要多個CollectionView
和一個UITableView
若是這些視圖的Delegate
都由ViewController
來實現,天然顯得臃腫且混亂。
通常手寫派會分出3個ChildViewController
來解決臃腫問題,難道Storyboard
就作不到麼?
答案是否認的,很早的版本,蘋果就給出了上圖中的解決方案。一個佔位的容器視圖指向子控制器的Embed Segue
按住Control
鍵連線到想要包含的子控制器,佔位視圖的實例==子控制器的view
(子控制器根視圖)
選擇Embed
連線方式後,子控制器 的尺寸變化成跟佔位視圖同樣的尺寸
這樣咱們能夠將功能圖標的CollectionView
的代碼放到這第一個子控制器上,CollectionViewDelegate
、CollectionViewDataSource
等代碼也由子控制器實現
同理,優惠專區能夠再添加一個Container View
,指向第二個子控制器。
Container View
建立的ChildViewController
如何與主ViewController
傳參或互相調用?
ChildViewController
能夠經過 self.parent(Swift)|| self.parentViewController(OC)來拿到主ViewController
的實例。 主ViewController
能夠經過 self.chilren(Swift) || self.childViewControllers(OC)來拿到ChildViewController
的實例,它是一個數組,順序等同於佔位視圖再視圖層次中的順序。
值得一提的是,經過此種方式建立的ChildViewController
,其構造方法晚於主ViewController
,但生命週期中的viewDidLoad
則早於主ViewController
, 所以在ChildViewController
中的viewDidLoad
方法中,self.parent 是nil
,這時不能拿到主ViewController
實例。若是須要在初始化的時候拿到主ViewController
的實例,則應該在主ViewController``viewDidLoad
方法中,調用ChildViewController
的特定方法,把 self 當參數傳過去。
將它添加到控制器之上。
它的本質是一個繼承自NSObject的子類,咱們徹底能夠把它當成一個小功能模塊的控制器。
class FeaturesController: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
@IBOutlet weak var collectionView:UICollectionView!
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
<#code#>
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
<#code#>
}
}
複製代碼
在Storyboard
上選中這個Object
,綁定上面的類
Object
,在彈出的菜單中連線
右鍵CollectionView
設置 Delegate 和 DataSource 等的連線
在主ViewController
中如需調用這個模塊的方法或者傳參
class HomeController: UIViewController {
@IBOutlet weak var featuresController:FeaturesController!
override func viewDidLoad() {
super.viewDidLoad()
featuresController.datas = [....]
featuresController.collectionView.reloadData()
}
}
複製代碼
完成連線,同理,若是一個頁面須要多個子模塊,能夠在Storyboard
上拖入多個Object
,並綁定不一樣的模塊控制類,相對於佔位的Container View
和ChildViewController
方法,Object
方法在傳參或互相調用方面,更加簡便。缺點是沒有ChildViewController
的生命週期方法,如需使用viewWillAppear
等,須要在主ViewController
的viewWillAppear
中,調用Object
的自定義方法。
經過上面的2種方法不難看出,並不是是Storyboard
形成ViewController
代碼臃腫,而是由於設計不當致使,就算你不用Storyboard
,把全部功能都寫在一個ViewController
裏同樣臃腫。這都是使用者決定的,並不是Storyboard
的責任!
StoryBoard
和 Xib
出了問題不容易測試?這個問題其實問的很模糊,我也是諮詢了不少人才知道,他們所謂的問題不容易測試,是指以下兩種狀況:
- 修改或刪除 @IBOutlet 的變量名時,對應的
Storyboard
上未作處理,致使運行時崩潰,崩潰內容看不懂!- 綁定的類名改變時,對應的
Storyboard
上未作處理,致使運行時崩潰,崩潰內容看不懂!
其實只要知道,蘋果是如何把Storyboard
的xml
解析成視圖,崩潰的錯誤內容也就容易看懂了 以前提到過,視圖構造使用的是下面這個方法
required init?(coder aDecoder: NSCoder) {
}
複製代碼
若是綁定的類名改變輸出錯誤:
- Unknown class _TtC11ProjectName14HomeController in Interface Builder file. // Swift
- Unknown class HomeController in Interface Builder file. // Objective C
經過上面的錯誤提示Interface Builder file
就是指經過Storyboard
或者Xib
構建視圖或者控制器,但找不到名爲HomeController
的控制器,看到這裏就應該明白,咱們某個Storyboard
上綁定了名爲HomeController
的控制器,但代碼中找不到,多是更名或者刪除了。這時能夠全局搜素一下
在搜出來的結果中能夠看到,是在Main.storyboard
上綁定了HomeController
,Test.swift
文件中定義了該類,可是由於更名因此沒法找到。
這樣的問題不用Storyboard
就能夠避免麼?答案是否認的,由於重構代碼的時候,改了一處忽略它處的例子比比皆是。哪怕純代碼也是同樣,所以,若是須要修改類名或者變量名,應該善用Xcode
的重構功能,而不是簡單的直接修改。
這樣修改類名或者變量名是,Storyboard
或者Xib
上綁定或連線的內容也會同步改變。就不會出錯了。
同理,@IBOutlet 連線的屬性經過下面的方法給視圖賦值
func setValue(_ value: Any?, forUndefinedKey key: String) {
......
}
複製代碼
若是變量名改變的時候,會出現以下錯誤:
- *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<HomeController 0x7fbd0ce20c40> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key featuresController.'
這個方法找不到對應的屬性時,就會拋出異常, 這裏就是指找不到featuresController
屬性,經過全局搜索能夠發現,代碼中改了名字,
解決的方法一樣是刪掉對應的連線或者修改變量名時使用重構
因而可知,所謂的不容易測試,徹底是由於重構不謹慎且對構造過程不理解,不然仍是很容易定位問題且修改的。並且重構代碼時利用Xcode重構功能
的話,連問題都不會出現
StoryBoard
和 Xib
下降執行效率?這個問題看起來好像是那麼回事,StoryBoard
和 Xib
本質上是XML
,要解析成視圖就須要反序列化,必然沒有直接代碼建立速度高,但這只是感受上,實際上有多少影響呢?咱們來測試一下:
var controllers:[ViewController] = []
let count = 30000
controllers.reserveCapacity(count)
guard let sb = storyboard else { return }
var beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = sb.instantiateViewController(withIdentifier: "ViewController") as! ViewController
controllers.append(vc)
}
print("Storyboard建立\(count)次用時", CACurrentMediaTime() - beginTime)
controllers.removeAll(keepingCapacity: true)
beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = ViewController()
controllers.append(vc)
}
print("純代碼建立\(count)次用時", CACurrentMediaTime() - beginTime)
複製代碼
第一次使用了3萬次,結果輸出
- Storyboard建立30000次用時 8.648092089919373
- 純代碼建立30000次用時 27.226440161000937
咱們看到了什麼?從Storyboard
建立居然比純代碼更快?簡直不敢相信本身的眼睛,並且差距這麼大必定是有什麼神奇的事情發生,爲了驗證個人想法,我又將Storyboard
建立複製了一次
var controllers:[ViewController] = []
let count = 30000
controllers.reserveCapacity(count)
guard let sb = storyboard else { return }
var beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = sb.instantiateViewController(withIdentifier: "ViewController") as! ViewController
controllers.append(vc)
}
print("Storyboard建立\(count)次用時", CACurrentMediaTime() - beginTime)
controllers = []
controllers.reserveCapacity(count)
beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = ViewController()
controllers.append(vc)
}
print("純代碼建立\(count)次用時", CACurrentMediaTime() - beginTime)
controllers = []
controllers.reserveCapacity(count)
beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = sb.instantiateViewController(withIdentifier: "ViewController") as! ViewController
controllers.append(vc)
}
print("Storyboard建立\(count)次用時", CACurrentMediaTime() - beginTime)
複製代碼
輸出結果以下,並且屢次運行結果相近,多是由於隨着內存使用率提升,電腦性能在下降,影響告終論,但無論怎麼說,大量測試空的ViewController
在這種狀況下確實比純代碼建立更快。
- Storyboard建立30000次用時 8.513293381780386
- 純代碼建立30000次用時 27.19225306995213
- Storyboard建立30000次用時 25.9916725079529
這個結果是如何出現的,不妨大膽猜想一下,多是因爲蘋果在對象屢次建立的狀況下,Storyboard
可能存在緩存復刻機制,來提高效率,而純代碼並無這樣的優化。爲了驗證猜想,咱們逐漸下降數量級。
- Storyboard建立3000次用時 0.20833597797900438
- 純代碼建立3000次用時 0.2654381438624114
- Storyboard建立3000次用時 0.34943647705949843
- Storyboard建立300次用時 0.010981905972585082
- 純代碼建立300次用時 0.005475352052599192
- Storyboard建立300次用時 0.014193600043654442
- Storyboard建立30次用時 0.0016030301339924335
- 純代碼建立30次用時 0.00031192018650472164
- Storyboard建立30次用時 0.001034758985042572
- Storyboard建立10次用時 0.0009886820334941149
- 純代碼建立10次用時 0.0001325791236013174
- Storyboard建立10次用時 0.0014422889798879623
上述結果果真驗證了咱們的猜想,隨着次數的減小,Storyboard
建立的速度逐漸低於存代碼建立,但單次耗時仍然低於萬分之一秒,這種效率是不會讓用戶有任何感知的,況且重複建立比純代碼還有優點,所以,這一條也不算StoryBoard
和 Xib
的缺點
StoryBoard
和 Xib
拖動和設置約束佈局很難精確?不易修改?我想,這種言論多是由於不太熟悉Interface Builder
的功能和操做形成的,僅僅實驗了幾回不得其門而入就放棄了。
實際上約束佈局是一個很強大的功能,能夠解決絕大多數(98%)佈局適配問題,98%
這個數並非隨便給出的,不少人以爲達不到這個比例是由於對約束理解較少,仍是按照之前的autolayoutMask的方式使用約束,所以不少佈局問題還在用代碼計算,可實際上約束功能十分強大,目前沒法經過約束直接解決,必須代碼輔助的問題微乎其微。
但與之相對的是約束的概念較多,依賴人腦思考很容易產生遺漏,這樣在運行的時候就會各類報錯或顯示異常,所以用純代碼寫約束,反覆運行調試視圖樣式尺寸十分常見,並且有些頁面較深,測試起來十分麻煩。
而使用StoryBoard
或 Xib
就不一樣了,缺乏約束或者約束衝突直接就有錯誤提示,適配不一樣設備能夠直接在Interface Builder
上切換測試,效率不知高了多少倍,準確性也高了不少
若是須要詳細瞭解在
StoryBoard
或Xib
上使用約束的技巧,能夠參考文章《純Swift項目-Xib | StoryBoard 設備適配技巧》及 《純Swift項目-Xib | StoryBoard 約束使用技巧》或其餘相關文章。
StoryBoard
和 Xib
雖然不是毫完好點,但優點遠大於付出,值得學習研究!