Swift 項目-Xib之StoryBoard 多人協作技巧
不同于國外,StoryBoard從面世到如今飽受國內開發(fā)者的質疑,質疑的理由很多,什么不利于多人協作啊,隱藏了UI細節(jié)啊,出問題不容易測試,降低執(zhí)行效率啊等等。此文就是針對這些問題的舉例和剖析。
StoryBoard 和 Xib 有什么區(qū)別?
StoryBoard 和 Xib 都是用來分離UI樣式代碼,改善視圖代碼重用率,增加所見即所得,降低視圖測試繁復度的視圖系列化工具。
- 其中Xib以視圖View為主。
- StoryBoard 和 Xib 有什么區(qū)別。
- StoryBoard 以控制器Controller及其之間的關系,以及和視圖View的關系為主。
StoryBoard 和 Xib 不利于多人協作,git合并代碼容易沖突,且難以處理?
這個是詆毀StoryBoard最多的理由,也是看上去最充分的理由。最顯著的就是下圖這種失敗的例子。
Storyboard不利圖片
在一個Storyboard中,大量的Controller控制器和Segue連線彰顯著錯綜復雜的UI關系,使人望而生畏或者難以維護。
但這并不應該是Storyboard的鍋,僅僅是使用者對工具的濫用!
沒錯,就是濫用,無論是Storyboard也好,純代碼也罷,它們的本質都是工具,工具本身沒有正義或邪惡,影響工具的是使用者。哪怕是用純代碼開發(fā),如果沒有命名規(guī)范,肆意的嵌套if,不遵守MVC或者MVVM等開發(fā)模式,不區(qū)分開發(fā)環(huán)境與生產環(huán)境,這樣寫出來的代碼又何談可維護性,和多人協作呢?
那么反過來說,如何使用Storyboard才不算濫用?
避免濫用,最好的方法就是定制規(guī)范,就好像代碼中的諸多規(guī)范一樣。每個團隊可能有自己不同的喜好,我在此拋磚引玉,列出我們團隊使用Storyboard的規(guī)范,供大家參考。
每個模塊獨立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的情況,就算配合失誤偶然發(fā)生,精簡的Storyboard其代碼量也不大,借助文件比較工具很容易就能處理git沖突。
說到底,臃腫的Storyboard和臃腫的ViewController一樣,都是難以維護且容易git沖突的。唯一的解決方案就是有節(jié)制的使用工具。
StoryBoard 和 Xib 隱藏了UI細節(jié),且容易導致ViewController臃腫?
與其說StoryBoard 和 Xib 隱藏了UI細節(jié),倒不如說蘋果是希望通過他們來引導開發(fā)者正確的使用視圖和控制器 ,他們創(chuàng)建視圖實例的時候都是通過
required init?(coder aDecoder: NSCoder) {
}
構造方法創(chuàng)建視圖實例。所有初始樣式都是在屬性面板中設置的值,通過
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等代碼也由子控制器實現;
同理,優(yōu)惠專區(qū)可以再添加一個Container View,指向第二個子控制器。
通過 Container View 創(chuàng)建的ChildViewController如何與主ViewController傳參或互相調用?
ChildViewController 可以通過 self.parent(Swift)|| self.parentViewController(OC)來拿到主ViewController的實例。主ViewController可以通過 self.chilren(Swift) || self.childViewControllers(OC)來拿到ChildViewController的實例,它是一個數組,順序等同于占位視圖再視圖層次中的順序。
值得一提的是,通過此種方式創(chuàng)建的ChildViewController,其構造方法晚于主ViewController,但生命周期中的viewDidLoad則早于主ViewController, 因此在ChildViewController中的viewDidLoad方法中,self.parent 是nil,這時不能拿到主ViewController實例。如果需要在初始化的時候拿到主ViewController的實例,則應該在主ViewController``viewDidLoad方法中,調用ChildViewController的特定方法,把self 當參數傳過去。
- 除此之外還可以使用Object對象。
將它添加到控制器之上。
它的本質是一個繼承自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: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key featuresController.'
這個方法找不到對應的屬性時,就會拋出異常, 這里就是指找不到featuresController屬性,通過全局搜索可以發(fā)現,代碼中改了名字。
解決的方法同樣是刪掉對應的連線或者修改變量名時使用重構。
由此可見,所謂的不容易測試,完全是因為重構不謹慎且對構造過程不理解,否則還是很容易定位問題且修改的。而且重構代碼時利用Xcode重構功能的話,連問題都不會出現。
StoryBoard 和 Xib 降低執(zhí)行效率?
這個問題看起來好像是那么回事,StoryBoard 和 Xib本質上是XML,要解析成視圖就需要反序列化,必然沒有直接代碼創(chuàng)建速度高,但這只是感覺上,實際上有多少影響呢?我們來測試一下:
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創(chuàng)建(count)次用時", CACurrentMediaTime() - beginTime)
controllers.removeAll(keepingCapacity: true)
beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = ViewController()
controllers.append(vc)
}
print("純代碼創(chuàng)建(count)次用時", CACurrentMediaTime() - beginTime)
第一次使用了3萬次,結果輸出:
- Storyboard創(chuàng)建30000次用時 8.648092089919373
- 純代碼創(chuàng)建30000次用時 27.226440161000937
我們看到了什么?從Storyboard創(chuàng)建竟然比純代碼更快?簡直不敢相信自己的眼睛,而且差距這么大一定是有什么神奇的事情發(fā)生,為了驗證我的想法,我又將Storyboard創(chuàng)建復制了一次:
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創(chuàng)建(count)次用時", CACurrentMediaTime() - beginTime)
controllers = []
controllers.reserveCapacity(count)
beginTime = CACurrentMediaTime()
for _ in 0..<count {
let vc = ViewController()
controllers.append(vc)
}
print("純代碼創(chuàng)建(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創(chuàng)建(count)次用時", CACurrentMediaTime() - beginTime)
輸出結果如下,而且多次運行結果相近,可能是因為隨著內存使用率提高,電腦性能在降低,影響了結論,但不管怎么說,大量測試空的ViewController在這種情況下確實比純代碼創(chuàng)建更快。
- Storyboard創(chuàng)建30000次用時 8.513293381780386
- 純代碼創(chuàng)建30000次用時 27.19225306995213
- Storyboard創(chuàng)建30000次用時 25.9916725079529
這個結果是如何出現的,不妨大膽猜測一下,可能是由于蘋果在對象多次創(chuàng)建的情況下,Storyboard可能存在緩存復刻機制,來提升效率,而純代碼并沒有這樣的優(yōu)化。為了驗證猜測,我們逐漸降低數量級。
1.Storyboard創(chuàng)建3000次用時 0.20833597797900438
2.純代碼創(chuàng)建3000次用時 0.2654381438624114
3.Storyboard創(chuàng)建3000次用時 0.34943647705949843
1.Storyboard創(chuàng)建300次用時 0.010981905972585082
2.純代碼創(chuàng)建300次用時 0.005475352052599192
3.Storyboard創(chuàng)建300次用時 0.014193600043654442
1.Storyboard創(chuàng)建30次用時 0.0016030301339924335
2.純代碼創(chuàng)建30次用時 0.00031192018650472164
3.Storyboard創(chuàng)建30次用時 0.001034758985042572
1.Storyboard創(chuàng)建10次用時 0.0009886820334941149
2.純代碼創(chuàng)建10次用時 0.0001325791236013174
3.Storyboard創(chuàng)建10次用時 0.0014422889798879623
上述結果果然驗證了我們的猜測,隨著次數的減少,Storyboard創(chuàng)建的速度逐漸低于存代碼創(chuàng)建,但單次耗時仍然低于萬分之一秒,這種效率是不會讓用戶有任何感知的,何況重復創(chuàng)建比純代碼還有優(yōu)勢,因此,這一條也不算StoryBoard 和 Xib的缺點。
在 StoryBoard 和 Xib 拖動和設置約束布局很難精確?不易修改?
我想,這種言論可能是因為不太熟悉Interface Builder的功能和操作造成的,僅僅實驗了幾次不得其門而入就放棄了。
實際上約束布局是一個很強大的功能,可以解決絕大多數(98%)布局適配問題,98%這個數并不是隨便給出的,很多人覺得達不到這個比例是因為對約束理解較少,還是按照以前的autolayoutMask的方式使用約束,因此很多布局問題還在用代碼計算,可實際上約束功能十分強大,目前無法通過約束直接解決,必須代碼輔助的問題微乎其微。
但與之相對的是約束的概念較多,依賴人腦思考很容易產生遺漏,這樣在運行的時候就會各種報錯或顯示異常,因此用純代碼寫約束,反復運行調試視圖樣式尺寸十分常見,而且有些頁面較深,測試起來十分麻煩。
而使用StoryBoard 或 Xib就不同了,缺少約束或者約束沖突直接就有錯誤提示,適配不同設備可以直接在Interface Builder上切換測試,效率不知高了多少倍,準確性也高了很多。
總結,StoryBoard 和 Xib雖然不是毫無缺點,但優(yōu)勢遠大于付出,值得學習研究!
參考資料:
[1]https://juejin.cn/post/6844903763526811655: https://juejin.cn/post/6844903763526811655
[2]https://juejin.cn/post/6844903763526811655: https://juejin.cn/post/6844903763526811655[3]
https://juejin.cn/post/6844903763526811655: https://juejin.cn/post/6844903763526811655