Swift開發黑科技:還在爭論MVC和MVVM?現在你應該試試MVP!

朵拉916 8年前發布 | 44K 次閱讀 Swift Apple Swift開發

 

WWDC2015已經過去一段時間了,我發現自從更新了Swift2.0到現在的Swift2.2,我只是跟著版本更新了所有需要更新的語法,依舊自以為是很熟練的Swift程序員。剛入職比較閑碰巧看到了1月份的中國首屆Swift大會上大牛們的分享,突然陷入了思考,有了很多新想法又重溫了幾遍WWDC2015大會的視頻,尤其是408和414號視頻!!!我下定決心重構自己的代碼,下面步入正題,結合Swift開發大會的一些分享,讓我們談談架構。
通過一個簡單的Demo:一個事件提醒的小應用。
這個應用會使用一個TableView混合展示一個時間段的所有待辦事宜和這個時間段的節日提醒,由于待辦事件和節日的數據構成是不同的,所以需要創建兩個模型,它們在TableView上展示的樣式也應該有所不同,很常規的我們還需要一個TableViewCell的子類。
現在數據工程里面的目錄是這樣的:
這里寫圖片描述

模型代碼:

struct Event {
    var date = ""
    var eventTitle = ""
    init(date:String,title:String){
        self.date = date
        self.eventTitle = title
    }
}

struct Festival {
    var date = ""
    var festivalName = ""
    init(date:String,name:String){
        self.date = date
        self.festivalName = name
    }
}

為了簡單我都使用了String類型的數據,至于為什么要使用struct而不使用class,大家可以參考WWDC2015的414號視頻,講的非常清楚,我自己的項目中的數據模型已經全部轉成struct了,我會在后面專門寫博文講解struct,這里就不贅述了。這里需要啰嗦一下,注意創建的時候使用的是字面量的方法,而不是可選型,我一直認為使用字面量的方法是更好的選擇,可選型很容易被當做語法糖濫用。尤其是數據的初始化中,你確定你真的需要一個空值?拿一個空值能做什么?做某種標志位么?請和你的后臺開發人員商議,讓他給你一個Bool類型的標志位,而不是一個空值。在可能的情況下,給你的數據模型的屬性賦一個語義明確的字面量初始值,比如這里我們使用空字符串作為初始值。如果你的數據只是做展示的不會存在修改情況,你也可以使用如下的方法做初始化,以達到效率的最大化:

struct Event {
    let date:String
    let eventTitle:String
    init(date:String = "",eventTitle:String = ""){
        self.date = date
        self.eventTitle = eventTitle
    }
}

在Swift1.2版本之后,let定義的數據也支持延遲加載了,這里使用了默認參數值做非空的保障。
模型否則在創建一個實例的時候各種可選型的解包或可選綁定會讓你吃盡苦頭,空值的訪問是程序carsh的元兇!
如果如果你更新了Xcode7.3,你會發現在創建一個屬性的時候Xcode的提示是“ =“,沒錯,Xcode推薦你用字面量去做初始化。
有了數據模型后,在Cell上創建兩個Label

class ShowedTableViewCell: UITableViewCell {
    //用來展示事件主題或節日名稱的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用來展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!


}

MVC架構:
從這里我們將展示傳統的MVC的寫法,但是包含了一些關鍵的知識點,所以還是建議您不要跳過。我們通過控制器中的代碼去控制數據的展示,由于數據源包含兩種數據類型,可以構造兩個數組避免數組的異構:

    var eventList = [Event]()
    var festivalList = [Festival]()
    let loadedEventList = [Event(date: "2月14", eventTitle: "送禮物")]
    let loadedFestivalList = [Festival(date: "1月1日", festivalName: "元旦"),Festival(date: "2月14", festivalName: "情人節")]

這里使用了struct的默認構造器構造對象,有兩個節日提醒:元旦節和情人節,元旦節沒什么事情做,情人節那天有個事件提醒”送禮物“,我們使用GCD去模擬數據刷新,整個控制器的代碼如下:

import UIKit

let cellReusedID = "ShowedTableViewCell"
class ShowedTableViewController: UITableViewController {

    var eventList = [Event]()
    var festivalList = [Festival]()
    let loadedEventList = [Event(date: "2月14", eventTitle: "送禮物")]
    let loadedFestivalList = [Festival(date: "1月1日", festivalName: "元旦"),Festival(date: "2月14", festivalName: "情人節")]
    override func viewDidLoad() {
        super.viewDidLoad()
        let delayInSeconds = 2.0
        let popTime = dispatch_time(DISPATCH_TIME_NOW,
            Int64(delayInSeconds * Double(NSEC_PER_SEC)))
        dispatch_after(popTime, dispatch_get_main_queue()) { () -> Void in
            self.eventList = self.loadedEventList
            self.festivalList = self.loadedFestivalList
            self.tableView.reloadData()
        }
    }



    // MARK: - Table view data source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return eventList.count + festivalList.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        //傳統的MVC,你需要在這里處理數據本身的同構與異構情況,還得處理數據與視圖的邏輯關系
        //這里我們把事件提醒放在節日的前面展示
        if indexPath.row > eventList.count - 1{
            cell.MixLabel.text = festivalList[indexPath.row - eventList.count].festivalName
            cell.dateLabel.text = festivalList[indexPath.row - eventList.count].date
            cell.backgroundColor = UIColor.whiteColor()
            return cell
        } else {
            cell.MixLabel.text = eventList[indexPath.row].eventTitle
            cell.dateLabel.text = eventList[indexPath.row].date
            cell.backgroundColor = UIColor.redColor()
            return cell
        }
    }


}

運行一下看看:
這里寫圖片描述

似乎還不錯,我們把兩個不同的數據結構展現在一張頁面上了,并且復用了cell,但是設置cell的代理方法中的代碼似乎有點多,而且如果我需要按照時間去排序,那么兩個同構的數組作為數據源不好排序,那么重構首先從把同構變成異構開始。由于struct沒有繼承,按照Swift2.0的精神,此時我們需要提煉兩個數據模型的共性,方法是利用protocol,觀察到Event和Festival都有date屬性,所以寫一個協議:

protocol hasDate{
    var date:String {get}
}

這里這個協議只有一個屬性date,Swift協議中定義的屬性只有聲明,遵守協議的對象必須實現這個屬性,但是不限于存儲屬性還是計算屬性。協議中定義的屬性必須指定最低的訪問級別,這里的date必須是可讀的,至于可寫的權限取決于實現該協議的數據類型中的定義。由于我們的Event和Festival都具有了date屬性,直接讓二者遵守hasDate協議,不要用擴展的方式讓二者遵守協議,編譯器報錯的,很怪0 0.
修改并化簡控制器中的數據源,使用異構數據源,現在控制器的代碼如下:

import UIKit

let cellReusedID = "ShowedTableViewCell"
class ShowedTableViewController: UITableViewController {

    var dataList = [hasDate]()
    var loadeddataList:[hasDate] = [Event(date: "2月14", eventTitle: "送禮物"),Festival(date: "1月1日", festivalName: "元旦"),Festival(date: "2月14", festivalName: "情人節")]
    override func viewDidLoad() {
        super.viewDidLoad()
        let delayInSeconds = 2.0
        let popTime = dispatch_time(DISPATCH_TIME_NOW,
            Int64(delayInSeconds * Double(NSEC_PER_SEC)))
        dispatch_after(popTime, dispatch_get_main_queue()) { () -> Void in
            //注意這里,我故意把loadeddataList中的數據打亂了,為了實現異構數據的按照某個公共類型的屬性的排序,使用了Swift內置的sort函數,String遵守了Compareable協議,這里為了簡單吧date指定為String類型,如果是NSDate,你可以在sort的閉包中指定合適的排序規則。
            self.dataList = self.loadeddataList.sort{$0.date < $1.date}
            self.tableView.reloadData()
        }
    }



    // MARK: - Table view data source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataList.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        //注意這里,通過可選綁定進行異構數據的類型控制
        if let event = dataList[indexPath.row] as? Event{
            cell.MixLabel.text = event.eventTitle
            cell.dateLabel.text = event.date
            cell.backgroundColor = UIColor.redColor()
            return cell
        } else if let festival = dataList[indexPath.row] as? Festival{
            cell.MixLabel.text = festival.festivalName
            cell.dateLabel.text = festival.date
            cell.backgroundColor = UIColor.whiteColor()
            return cell
        } else {
            return cell
        }
    }
}

運行一下:
這里寫圖片描述
沒有任何問題。對異構數組的類型判斷的寫法來自于WWDC2015上的408號視頻,現在控制器里的代碼已經精簡了很多了,我們解決了異構的問題,對于MVC來說,這似乎已經精簡到極限了。這是一個簡單的Demo,在真正的工程中一個控制器當中的代碼可能有幾百上千行,或者有多個TableView,這個時候MVC的弊端就顯現了,在幾百行代碼中可能有一百行都用來做數據與視圖的綁定,而數據模型和視圖本身的代碼定義中卻只有寥寥數十行,控制器的負擔太重了!因此有人提出了將控制器中有關模型與視圖的邏輯的代碼提出到一個單獨的區域進行處理,這就是MVVM架構的由來。
MVVM架構
對MVVM架構的解讀我想引用Swift開發者大會上李信潔前輩的示例寫法,通過POP來實現一個MVVM,并且對其寫法進行了一些精簡。我們先不修改View和Modal的代碼,因為需要更新的是一個cell,所以首先需要寫一個傳遞Modal中數據的協議:

protocol CellPresentable{
    var mixLabelData:String {get set}
    var dateLabelData:String {get set}
    var color: UIColor {get set}
    func updateCell(cell:ShowedTableViewCell)
}

這個協議的思想是顯示地聲明一個更新cell的方法,并根據cell需要的數據聲明兩個屬性,我們并不關心mixLabel和dateLabel的數據從哪里來,叫什么名字,但他們的功能是確定的,Swift2.0之后可以擴展協議,下面通過協議擴展給這個協議增加默認的實現,這樣在綁定數據時可以減少代碼量:

extension CellPresentable{
    func updateCell(cell:ShowedTableViewCell){
        cell.MixLabel.text = mixLabelData
        cell.dateLabel.text = dateLabelData
        cell.backgroundColor = color
    }
}

好了,我們寫好了,下一步我們要修改cell的代碼,增加一個方法接受一個CellPresentable:

class ShowedTableViewCell: UITableViewCell {
    //用來展示事件主題或節日名稱的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用來展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!

    func updateWithPresenter(presenter: CellPresentable) {
        presenter.updateCell(self)
    }
}

這里也做了一些改進,李信潔前輩的示例中是針對每一個控件去定義方法的,其實對一個View的所有IBOutlet做更新不就是更新它自己么,所以這里我的寫法是直接傳入self。然后(我也不想多說然后,但是步驟就是這么多)為了綁定異構的Model和View你還需要定義一個ViewModel,并且通過定義不同的init實現數據綁定:

struct ViewModel:CellPresentable{
    var dateLabelData = ""
    var mixLabelData = ""
    var color = UIColor.whiteColor()
    init(modal:Event){
        self.dateLabelData = modal.date
        self.mixLabelData = modal.eventTitle
        self.color = UIColor.redColor()
    }
    init(modal:Festival){
        self.dateLabelData = modal.date
        self.mixLabelData = modal.festivalName
        self.color = UIColor.whiteColor()
    }
}

最后我們終于可以去修改我們的控制器了,控制器中需要更改的是與cell有關的datasource方法:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        if let event = dataList[indexPath.row] as? Event{
            let viewModel = ViewModel(modal: event)
            cell.updateWithPresenter(viewModel)
            return cell
        } else if let festival = dataList[indexPath.row] as? Festival{
            let viewModel = ViewModel(modal: festival)
            cell.updateWithPresenter(viewModel)
            return cell
        } else {
            return cell

        }
    }
}

這段代碼寫的我滿頭大汗,編譯運行,幸運的是運行的結果是正確的:
這里寫圖片描述

我在想MVVM模式的意義是什么?我在使用MVVM之前甚至需要考慮一下值不值得花時間去寫成MVVM的模樣,因為MVVM需要給所有的view提供協議,并且將所有的數據模型的綁定過程寫進一個新的數據結構ViewModal中,但其實這個ViewModel的價值非常之小,除了數據綁定,沒有其他作用了,里面甚至只有空洞的init構造器,我想我已經決定放棄這個思路了。
MVP的萌芽階段
我繼續著自己的思考,大會上傅若愚前輩分享的示例給了我很大的啟發,因為他提供了一個沒有中間層的模型!我一直在思考這個模型,并且在入職的第一個項目中一直在按照他的模型來組織自己的代碼,直到我頓悟了自己的MVP模型。下面簡單介紹一下傅若愚前輩的思路,這個思路的優勢在于所有的數據和模型綁定都只需要兩個通用的協議:

//視圖使用的協議
protocol RenderContext{
    func renderText(texts:String...)
    func renderImage(images:UIImage...)
}
//數據使用的協議
protocol ViewModelType{
    func renderInContext(context:RenderContext)
}

上面是大會上傅若愚前輩的原版,在介紹這個協議的用法之前,我覺得應該先做一點點改進,ViewModalType應該改成:

protocol ViewModelType{
    func renderInContext<R:RenderContext>(context:R)
}

這兩個版本都可以通過編譯,差別在運行的效率上,下面我在playground中展示一個示例,這個示例來源于《Advanced Swift》這本書,其實蘋果的WWDC2015 408號視頻中也明確表述了不要把協議當做參數類型,而寫成泛型的約束,但是沒有詳細講解為什么,下面是示例:

func takesProtocol(x: CustomStringConvertible) { //
    print ( sizeofValue(x))
}
func takesPlaceholder<T: CustomStringConvertible>(x: T) {
    print ( sizeofValue(x))
}

兩個方法,前者使用協議作為參數的類型,后者使用協議作為泛型的約束條件,兩個方法都會打印參數的長度,調用一下試試:

takesProtocol(1 as Int16) takesPlaceholder(1 as Int16)

打印結果:
這里寫圖片描述
換成類再打印一次:
這里寫圖片描述
沒錯,由于協議本身既可以被類遵守、也可以被結構體、枚舉遵守,也就是說既可以被引用類型遵守也可以被值類型遵守,把協議當做參數類型,實際上會創造一個Box類型,里面會為引用類型遵守者預留地址也會為值類型遵守者預留地址,甚至需要存儲一個指針長度找到協議的真正繼承類型。而Swift2.0之后編譯器得到了加強,具有了泛型特化的功能,對代碼中的泛型在編譯時就會確定其真正的類型,不耗費任何性能。
下面我們用改造后的傅若愚前輩的協議來改造Demo,你需要讓你的數據模型去遵守RenderContext,然后根據模型的參數類型將每一個參數存入對應類型方法的參數列表中,這些方法都是可變參數,不限制數量,但是參數的類型是確定的。這種使用參數類型做通用類型的寫法消滅了中間的ViewModel層,把Model和View直接對接了。由于Swift要求每一個協議的遵守者都必須實現協議的全部方法,而有些方法的數據模型并沒有,所以你在使用之前需要使用協議擴展為這些方法實現一個空的實現:

protocol RenderContext{
    func renderText(texts:String...)
    func renderImage(images:UIImage...)
}

extension RenderContext{
    func renderText(texts:String...){

    }
    func renderImage(images:UIImage...){

    }
}

現在你的模型應該是下面這樣:

struct Event:hasDate,ViewModelType{
    var date = ""
    var eventTitle = ""
    func renderInContext<R : RenderContext>(context: R) {
        context.renderText(date,eventTitle)
    }
}

struct Festival:hasDate,ViewModelType{
    var date = ""
    var festivalName = ""
    func renderInContext<R : RenderContext>(context: R) {
        context.renderText(date,festivalName)
    }
}

視圖的代碼應該是這樣的:

class ShowedTableViewCell: UITableViewCell,RenderContext {
    //用來展示事件主題或節日名稱的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用來展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!

    func renderText(texts: String...) {
        dateLabel.text = texts[0]
        MixLabel.text = texts[1]
    }
}

由于遵守了多個協議,所以控制器中原本的異構類型不合適了,此時可以給多個協議類型寫一個別名方便使用,記得順便更新一下你的Model,提高可讀性:

typealias DateViewModel = protocol<hasDate,ViewModelType>

現在控制器中的數據源可以使用新的異構類型了:

var dataList = [DateViewModel]()
    var loadeddataList:[DateViewModel] = [Event(date: "2月14", eventTitle: "送禮物"),Festival(date: "1月1日", festivalName: "元旦"),Festival(date: "2月14", festivalName: "情人節")]

然后更新cell的代理方法:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        dataList[indexPath.row].renderInContext(cell)
        return cell
    }

不錯,代碼簡潔了很多,運行一下:
這里寫圖片描述
等等,我們似乎遺漏了一些東西,cell的背景顏色呢?好吧讓我們加上,可是我該去哪里加呢?去控制器中嗎?不不堅決不能碰控制器,那么只能去cell中了,現在問題出現了,當兩個模型共享一個視圖的時候,我該如何判斷數據源從哪里來?renderText(texts: String…)這樣的寫法已經完全失去了異構的特性,那么試著這樣寫,在數據傳遞參數的時候多傳一個String好了,反正參數是我們的自由:

struct Event:DateViewModel{
    var date = ""
    var eventTitle = ""
    func renderInContext<R : RenderContext>(context: R) {
        context.renderText(date,eventTitle,"red")
    }
}

這樣在檢驗的時候就看最后一個參數就好了:

class ShowedTableViewCell: UITableViewCell,RenderContext {
    //用來展示事件主題或節日名稱的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用來展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!

    func renderText(texts: String...) {
        dateLabel.text = texts[0]
        MixLabel.text = texts[1]
        if texts[2] == "red"{
            backgroundColor = UIColor.redColor()
        }
    }
}

這里有個語法糖,可變參數的方法,在取參時不會發生越界,因為Festival的renderText方法只傳了兩個值,運行結果又正常了。那么如果我粗心把參數寫錯順序了呢?結果成了這樣:
這里寫圖片描述
如果我的Festival中多了一個Int類型,而Event中恰巧沒有呢?按照值去區分參數不是一個好主意,因為你用下標從一個數組中取值的時候除了它的類型不能得到任何信息,甚至都不知道這個值存不存在!我再次陷入了思考,既然View需要的是Model中的屬性,這不就等于需要Model自己么,那么為什么我們不能直接傳遞Modal自己呢?
MVP!
所以我再次改造了傅若愚前輩的協議,順便把名字改的好辨認一點,原來的名字太容易出錯了- -現在它是這樣子的:

//視圖使用的協議
protocol ViewType{
    func getData<M:ModelType>(data:M)
}
//數據使用的協議
protocol ModelType{
    func giveData<V:ViewType>(context:V)
}

不需要在擴展中寫默認實現,因為傳值是相互且確定的,所以方法一定會被實現。
模型是這樣子的:

typealias DateViewModel = protocol<hasDate,ModelType>
struct Festival:DateViewModel{
    var date = ""
    var festivalName = ""
    func giveData<V : ViewType>(context: V) {
        context.getData(self)
    }
}

struct Event:DateViewModel{
    var date = ""
    var eventTitle = ""
    func giveData<V : ViewType>(context: V) {
        context.getData(self)
    }
}

視圖:

class ShowedTableViewCell: UITableViewCell,ViewType {
    //用來展示事件主題或節日名稱的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用來展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!

    func getData<M : ModelType>(data: M) {
        if let event = data as? Event{
            MixLabel.text = event.eventTitle
            dateLabel.text = event.date
            backgroundColor = UIColor.redColor()
        } else if let festival = data as? Festival{
            MixLabel.text = festival.festivalName
            dateLabel.text = festival.date
        }
    }
}

再次用蘋果官方給出的異構判斷方法解決異構,協議不同于類,沒有那么多繼承上的檢查,所以使用as?是很高效的,最后只要給控制器中的代碼換個名字就夠了:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        dataList[indexPath.row].giveData(cell)
        return cell
    }

完成,運行效果:
這里寫圖片描述
如果還沒有人發現這個架構,我想起個名字:MVP(Model-View-Protocol),有趣又貼切。

寫在后面:
博主欠了欠身子,從吃完晚飯寫到了半夜,一口氣完成了本文,如果你喜歡我的文章并且得到了啟發,歡迎轉載傳閱,注明出處即可。在Swift1.X時代我覺得Swift脆弱的像只小貓,Swift2.0之后我才突然發現蘋果締造的是一只野獸。通過不斷鍛煉自己面向協議編程的能力,我有了很多新的體會,想起了迪杰斯特拉老爺子著名的goto有害論,請準許我大膽預言一下,在面向協議的世界中AnyObject也是有害的,點到為止。

關于博主本人:
《Swift開發手冊:技巧與實戰》作者。國內計算機領域的某名校畢業,學習不差,初入社會,曾只身離校北漂妄圖以退學抗議畸形的研究生教育,后心疼父母返校完成學業。從2014年底開始接觸Swift后一發不可收拾,至今保持狂熱,小人物大夢想,孜孜不倦致力于改善iOS編程體驗。歡迎大家留言交流,力所能及之處,必傾囊相授。

來自: http://blog.csdn.net/cg1991130/article/details/51116120

 本文由用戶 朵拉916 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!