純Swift項目-Xib | StoryBoard 多人協做技巧

不一樣於國外,StoryBoard從面世到現在飽受國內開發者的質疑,質疑的理由不少,什麼不利於多人協做啊,隱藏了UI細節啊,出問題不容易測試,下降執行效率啊等等。此文就是針對這些問題的舉例和剖析。git

StoryBoardXib 有什麼區別?

StoryBoardXib 都是用來分離UI樣式代碼,改善視圖代碼重用率,增長所見即所得,下降視圖測試繁複度的視圖系列化工具,swift

  1. 其中Xib以視圖View爲主,
  2. StoryBoard 以控制器Controller及其之間的關係,以及和視圖View的關係爲主。

實際使用例子參見《純Swift項目-Xib | StoryBoard 設備適配技巧》或其餘StoryBoard文章數組

StoryBoardXib 不利於多人協做,git合併代碼容易衝突,且難以處理?

這個是詆譭StoryBoard最多的理由,也是看上去最充分的理由。最顯著的就是下圖這種失敗的例子。緩存

Storyboard不利圖片

在一個Storyboard中,大量的Controller控制器和Segue連線彰顯着錯綜複雜的UI關係,令人望而生畏或者難以維護。app

但這並不該該是Storyboard的鍋,僅僅是使用者對工具的濫用!

沒錯,就是濫用,不管是Storyboard也好,純代碼也罷,它們的本質都是工具,工具自己沒有正義或邪惡,影響工具的是使用者。哪怕是用純代碼開發,若是沒有命名規範,肆意的嵌套if,不遵照MVC或者MVVM等開發模式,不區分開發環境與生產環境,這樣寫出來的代碼又何談可維護性,和多人協做呢?ide

那麼反過來講,如何使用Storyboard纔不算濫用?

避免濫用,最好的方法就是定製規範,就好像代碼中的諸多規範同樣。每一個團隊可能有本身不一樣的喜愛,我在此拋磚引玉,列出咱們團隊使用Storyboard的規範,供你們參考。工具

每一個模塊獨立Storyboard 每一個Storyboard只應該有一個主VC和同頁的子VC,主VC不該存在2個以上
  1. 一個項目中,Storyboard不應是孤立存在的,應該像MVP模式那樣,每一個頁面都有獨立的Storyboard,每一個Storyboard只應該有一個主VC和同頁的子VC,主VC不該存在2個以上。(絕大多數狀況下,一個Storyboard上只應該有一個VC
  2. 頁面間的Segue連線應該使用Stroyboard Reference SceneUITabBarController的子頁由於複雜度應該當成主VC處置
  3. 視圖的初始樣式應儘可能在Storyboard上屬性面板中設置,非極特殊狀況,佈局也應在Storyboard上使用各類約束配合完成。這樣有利於視圖樣式和視圖代碼分離,有利於視圖代碼重用性和兼容性提升。
  4. 對於邏輯複雜的VC,應添加Object對象,並綁定相應的類來分離邏輯代碼。
  5. 對於圓角,背景色,陰影等CALayer的樣式,應該使用擴展或子類化實例的形式,使用@IBInspectable屬性關鍵字,在Storyboard屬性面板中設定初始樣式。
  6. 對於自定義視圖,應使用@IBDesignable關鍵字保障在在Storyboard上所見即所得!

使用以上原則,只要任務分工合理,基本上不存在多人同時修改同一個Storyboard的狀況,就算配合失誤偶然發生,精簡的Storyboard其代碼量也不大,藉助文件比較工具很容易就能處理git衝突。佈局

說到底,臃腫的Storyboard和臃腫的ViewController同樣,都是難以維護且容易git衝突的。惟一的解決方案就是有節制的使用工具。post

StoryBoardXib 隱藏了UI細節,且容易致使ViewController臃腫?

與其說StoryBoardXib 隱藏了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的代碼放到這第一個子控制器上,CollectionViewDelegateCollectionViewDataSource等代碼也由子控制器實現

同理,優惠專區能夠再添加一個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.parentnil,這時不能拿到主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 設置 DelegateDataSource 等的連線

在主ViewController中如需調用這個模塊的方法或者傳參

class HomeController: UIViewController {
    
    @IBOutlet weak var featuresController:FeaturesController!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        featuresController.datas = [....]
        featuresController.collectionView.reloadData()
    }
    
}
複製代碼

完成連線,同理,若是一個頁面須要多個子模塊,能夠在Storyboard上拖入多個Object,並綁定不一樣的模塊控制類,相對於佔位的Container ViewChildViewController方法,Object方法在傳參或互相調用方面,更加簡便。缺點是沒有ChildViewController的生命週期方法,如需使用viewWillAppear等,須要在主ViewControllerviewWillAppear中,調用Object的自定義方法。

經過上面的2種方法不難看出,並不是是Storyboard形成ViewController代碼臃腫,而是由於設計不當致使,就算你不用Storyboard,把全部功能都寫在一個ViewController裏同樣臃腫。這都是使用者決定的,並不是Storyboard的責任!

StoryBoardXib 出了問題不容易測試?

這個問題其實問的很模糊,我也是諮詢了不少人才知道,他們所謂的問題不容易測試,是指以下兩種狀況:

  1. 修改或刪除 @IBOutlet 的變量名時,對應的Storyboard上未作處理,致使運行時崩潰,崩潰內容看不懂!
  2. 綁定的類名改變時,對應的Storyboard上未作處理,致使運行時崩潰,崩潰內容看不懂!

其實只要知道,蘋果是如何把Storyboardxml解析成視圖,崩潰的錯誤內容也就容易看懂了 以前提到過,視圖構造使用的是下面這個方法

required init?(coder aDecoder: NSCoder) {
    
    }
複製代碼

若是綁定的類名改變輸出錯誤:

  1. Unknown class _TtC11ProjectName14HomeController in Interface Builder file. // Swift
  2. Unknown class HomeController in Interface Builder file. // Objective C

經過上面的錯誤提示Interface Builder file就是指經過Storyboard或者Xib構建視圖或者控制器,但找不到名爲HomeController的控制器,看到這裏就應該明白,咱們某個Storyboard上綁定了名爲HomeController的控制器,但代碼中找不到,多是更名或者刪除了。這時能夠全局搜素一下

在搜出來的結果中能夠看到,是在Main.storyboard上綁定了HomeControllerTest.swift文件中定義了該類,可是由於更名因此沒法找到。

這樣的問題不用Storyboard就能夠避免麼?答案是否認的,由於重構代碼的時候,改了一處忽略它處的例子比比皆是。哪怕純代碼也是同樣,所以,若是須要修改類名或者變量名,應該善用Xcode的重構功能,而不是簡單的直接修改。

這樣修改類名或者變量名是,Storyboard或者Xib上綁定或連線的內容也會同步改變。就不會出錯了。

同理,@IBOutlet 連線的屬性經過下面的方法給視圖賦值

func setValue(_ value: Any?, forUndefinedKey key: String) {
        ......
    }
複製代碼

若是變量名改變的時候,會出現以下錯誤:

  1. *** 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重構功能的話,連問題都不會出現

StoryBoardXib 下降執行效率?

這個問題看起來好像是那麼回事,StoryBoardXib本質上是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萬次,結果輸出

  1. Storyboard建立30000次用時 8.648092089919373
  2. 純代碼建立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在這種狀況下確實比純代碼建立更快。

  1. Storyboard建立30000次用時 8.513293381780386
  2. 純代碼建立30000次用時 27.19225306995213
  3. Storyboard建立30000次用時 25.9916725079529

這個結果是如何出現的,不妨大膽猜想一下,多是因爲蘋果在對象屢次建立的狀況下,Storyboard可能存在緩存復刻機制,來提高效率,而純代碼並無這樣的優化。爲了驗證猜想,咱們逐漸下降數量級。

  1. Storyboard建立3000次用時 0.20833597797900438
  2. 純代碼建立3000次用時 0.2654381438624114
  3. Storyboard建立3000次用時 0.34943647705949843
  1. Storyboard建立300次用時 0.010981905972585082
  2. 純代碼建立300次用時 0.005475352052599192
  3. Storyboard建立300次用時 0.014193600043654442
  1. Storyboard建立30次用時 0.0016030301339924335
  2. 純代碼建立30次用時 0.00031192018650472164
  3. Storyboard建立30次用時 0.001034758985042572
  1. Storyboard建立10次用時 0.0009886820334941149
  2. 純代碼建立10次用時 0.0001325791236013174
  3. Storyboard建立10次用時 0.0014422889798879623

上述結果果真驗證了咱們的猜想,隨着次數的減小,Storyboard建立的速度逐漸低於存代碼建立,但單次耗時仍然低於萬分之一秒,這種效率是不會讓用戶有任何感知的,況且重複建立比純代碼還有優點,所以,這一條也不算StoryBoardXib的缺點

StoryBoardXib 拖動和設置約束佈局很難精確?不易修改?

我想,這種言論多是由於不太熟悉Interface Builder的功能和操做形成的,僅僅實驗了幾回不得其門而入就放棄了。

實際上約束佈局是一個很強大的功能,能夠解決絕大多數(98%)佈局適配問題,98%這個數並非隨便給出的,不少人以爲達不到這個比例是由於對約束理解較少,仍是按照之前的autolayoutMask的方式使用約束,所以不少佈局問題還在用代碼計算,可實際上約束功能十分強大,目前沒法經過約束直接解決,必須代碼輔助的問題微乎其微。

但與之相對的是約束的概念較多,依賴人腦思考很容易產生遺漏,這樣在運行的時候就會各類報錯或顯示異常,所以用純代碼寫約束,反覆運行調試視圖樣式尺寸十分常見,並且有些頁面較深,測試起來十分麻煩。

而使用StoryBoardXib就不一樣了,缺乏約束或者約束衝突直接就有錯誤提示,適配不一樣設備能夠直接在Interface Builder上切換測試,效率不知高了多少倍,準確性也高了不少

若是須要詳細瞭解在StoryBoardXib上使用約束的技巧,能夠參考文章《純Swift項目-Xib | StoryBoard 設備適配技巧》及 《純Swift項目-Xib | StoryBoard 約束使用技巧》或其餘相關文章。

總結,StoryBoardXib雖然不是毫完好點,但優點遠大於付出,值得學習研究!

相關文章
相關標籤/搜索