淺析iOS開發的那些架構:MVC/MVP/MVVM

前言

很早以前就想總結一下,iOS開發中常用的一些架構:MVC、MVP、MVVM;但是一直感覺自己沒有理解透徹,因爲發現自己理解的和網上其他人的總是有出入;網上的衆說紛紜,仁者見仁智者見智;
隨着經驗的增長,自己對於這些架構的理解每次都有不同的收穫,漸漸的可能和最初瞭解的情況大相徑庭;
現在轉念一想,架構這些事情並沒有絕對的對錯,也不會有什麼標準答案;每個人都會結合自己的經驗加以理解,實踐出最符合自己項目的架構;只要理解這些架構的底層邏輯、運用其解決項目中的問題,那就不用在乎具體的招式是什麼了;

下面就談談我對MVC/MVP/MVVM的理解

MVC

MVC (Model-View-Controller) 是蘋果推薦的架構模式,也是其默認使用的架構模式。Apple官方MVC架構定義如下:

圖示中簡單的列出了各層間的關係;結合iOS開發中實際場景,引用斯坦福的CS193p Paul老師的經典MVC圖,更加清晰的說明各層間的通信:

各層職責

  • Model
    業務模型層
    Model封裝了應用程序的數據,也負責數據的獲取及數據的處理
    用戶在View中所進行的創建或修改數據的操作,通過Controller傳達出去直接更新Model;Model數據更改時,它通過KVO或NotificationCenter等方式通知Controller,Controller再更新相應的View。

  • View
    視圖層
    應用程序中用戶可以看見的對象都屬於View層,對於iOS來說所有以UI開頭的類基本都是這一層;View層負責界面元素表達(包括動畫效果)及響應用戶操作;
    Controller收到Model的更新通知後,通過引用關係直接更新View;View層所需要的顯示數據,Controller可以通過dataSource提供;View響應事件後通過delegate或Target-Action等方式反饋給Controller處理;

  • Controller
    控制器層
    它相當於Model和View的中間人,負責Model和View的相互調配:當Model數據更改時更新對應的視圖,當View更新或操作後更新對應的數據;

另外Model層和View層是沒有任何直接的關係的,它們之間的通信都由Controller完成;

MVC架構的總的作用也體現出來了:

  1. 減少耦合性:各層分工明確,降低了相互的關聯,方便維護
  2. 提高了代碼重用性:Model層和View層解耦了,方便重用
  3. 便於測試:在正確使用Model層的情況下,業務處理和View、Controller完全解耦,可以單獨測試業務邏輯;

MVC的困惑

但是在iOS實際開發中,慢慢的我們會發現大部分人寫的MVC已經偏離了理想中的架構設計;

一個明顯的特徵就是ViewController層變得特別臃腫,代碼異常的多,形成了另一種MVC:Massive ViewController;

原因有很多,主要在於2點:

  • Controller層包含View的顯示邏輯
  • Model層誤解、誤用

下面通過一個demo來分析:MVC實現一個簡單的新聞列表界面;

這樣的實現代碼,只要入門級就能實現;

// model層  由服務器返回的數據:標題、時間、封面
struct News {
    let title: String
    let createTime: String
    let coverSrc: String
}
// view層,顯示新聞數據
class NewsTableViewCell: UITableViewCell {

    let titleLabel = UILabel()
    let dateLabel = UILabel()
    let coverImageView = UIImageView()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        backgroundColor = .white
        
        titleLabel.textColor = .black
        dateLabel.textColor = .gray
        coverImageView.contentMode = .scaleAspectFill
        addSubview(titleLabel)
        addSubview(dateLabel)
        addSubview(coverImageView)
        
        ...省略佈局代碼
    }
    
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
// viewController,核心代碼
class NewsListViewController: UIViewController {
    var newsList = Array<News>()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(tableView)
        view.addSubview(activityIndicator)
        
        activityIndicator.startAnimating()
        AF.request("https://c.3g.163.com/nc/article/list/T1467284926140/0-20.html").responseJSON { rsp in
            self.activityIndicator.stopAnimating()

            switch rsp.result {
            case .success(let json):
                print(json)
                let jsonData = JSON.init(json)
                let newsJsonArr = jsonData["T1467284926140"].arrayValue
                self.newsList = newsJsonArr.map {
                    let title = $0["title"].stringValue
                    let createTime = $0["ptime"].stringValue
                    let coverSrc = $0["imgsrc"].stringValue
                    return News(title: title, createTime: createTime, coverSrc: coverSrc)
                }
                self.tableView.reloadData()
            case .failure(let err):
                print(err)
            }
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell: NewsTableViewCell
        if let rs = tableView.dequeueReusableCell(withIdentifier: cellId) as? NewsTableViewCell {
            cell = rs
        } else {
            cell = NewsTableViewCell(style: .subtitle, reuseIdentifier: cellId)
        }
            
        let news = newsList[indexPath.row]
        cell.titleLabel.text = news.title
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
        let date = dateFormatter.date(from: news.createTime)
        cell.dateLabel.text = dateFormatter.string(from: date ?? Date.now)
        let url = URL(string: news.coverSrc.replacingOccurrences(of: "http:", with: "https:"))
        cell.coverImageView.kf.setImage(with: url)
        
        return cell
    }
}

針對以上代碼,分析存在的問題;

Controller包含View的顯示邏輯

根據命名就可以知道,ViewController並不是單獨的Controller而已,Apple的Cocoa框架把View和Cotroller組合在一起,ViewController同時做了View和Controller的事情;這是它和典型的MVC的不同之處,嚴格意義上來說也算違背了MVC架構的原則了;
實際上Cocoa中的MVC架構如下:

Cocoa爲何要這麼設計?

在服務端開發領域,Controller做完自己的事情之後,就把所有關於View的工作交給了頁面渲染引擎去做,Controller不會去做任何關於View的事情,包括生成View。這些都由渲染引擎代勞了。這是一個區別,但其實服務端View的概念和Native應用View的概念,真正的區別在於:從概念上嚴格劃分的話,服務端其實根本沒有View,拜HTTP協議所賜,我們平時所討論的View只是用於描述View的字符串(更實質的應該稱之爲數據),真正的View是瀏覽器。
所以服務端只管生成對View的描述,至於對View的長相,UI事件監聽和處理,都是瀏覽器負責生成和維護的。但是在Native這邊來看,原本屬於瀏覽器的任務也逃不掉要自己做。那麼這件事情由誰來做最合適?蘋果給出的答案是:UIViewController。
鑑於蘋果在這一層做了很多艱苦卓絕的努力,讓iOS工程師們不必親自去實現這些內容。而且,它把所有的功能都放在了UIView上,並且把UIView做成不光可以展示UI,還可以作爲容器的一個對象。
看到這兒你明白了嗎?UIView的另一個身份其實是容器!UIViewController中自帶的那個view,它的主要任務就是作爲一個容器

詳見 Casa Taloyum文章 iOS應用架構談 view層的組織和調用方案

在iOS開發中,View上面的事件都是回傳給ViewController,然後ViewController再另行調度,ViewController可以根據不同事件的產生去很方便地更改容器內容;如demo中,tableView的刷新,加載提示activityIndicator的顯示與否;
還有常用的,切換至無網絡、無數據頁面等等;

實際上iOS的MVC中Controller的職責會多幾個:

  • 負責View的生成
  • 負責管理View的生命週期

因此,Cocoa針對MVC這樣的處理是非常合適的;但是對於複雜界面,ViewController容易出現代碼膨脹,管理過多的狀態,響應代碼和邏輯代碼混淆一起,這將導致代碼很難維護,很難測試;

關於這種原因導致的ViewController臃腫,業界也有一套解決方案;
ViewController有一個self.view的視圖容器,那麼我們也可以把ViewController看成一個管理各個View的容器;簡單來說就是:將一個原業務ViewController拆分成容器coordinate vc和對應的業務child vc來協調工作;之前的vc有多個頁面的,這時就拆分成多個child vc;

  • child vc負責view的生成、響應view的事件,管理自己view的生命週期;
  • coordinate vc負責創建child vc,將child vc的視圖添加到自己的self.view容器上;同時管理view的生命週期、控制child vc獲取數據等操作;

這裏不再單獨寫demo演示,大家只要類比UITabBarController就明白啥意思了。
這種方案實質上就是將原先臃腫的vc平攤到每個child vc了,的確也是一種優化方式,但是如果頁面邏輯比較多的情況child vc也容易出現一樣的問題,那還得將child vc再拆分下去,而且vc多了也容易出現協調的代碼變多、複雜;是否使用這種方案,需要結合自己項目具體情況決定;

Model層誤解、誤用

如果說上面這個因素是因爲Apple本身設計引起,那 Model層誤解、誤用就完全是開發人員自己的原因了;

大部分人將Model理解成:只是單獨的數據模型;
如上面demo中News對象,只有和服務器數據對應的幾個字段的數據模型;如果只是數據模型,它也稱不上是層;
另一個問題表現是:Controller裏的var newsList = Array<News>(),我們等於把Model放到了Controller裏,Model無法與Controlle 進行有效的通訊 (MVC圖中的Notification & KVO 部分)

實際上Model層正確定義是業務模型層,也就是所有業務數據和業務邏輯都應該定義在Model層裏面。

由於將Model層只是當成了數據模型,導致了業務數據、邏輯在MVC架構下無處安放;最終這些代碼還是堆砌到Controller層了;

Model層的正確設計:

M層要完成對業務邏輯實現的封裝,一般業務邏輯最多的是涉及到客戶端和服務器之間的業務交互。M層裏面要完成對使用的網絡協議(HTTP, TCP,其他)、和服務器之間交互的數據格式(XML, JSON,其他)、本地緩存和數據庫存儲(COREDATA, SQLITE,其他)等所有業務細節的封裝,而且這些東西都不能暴露給C層。所有供C層調用的都是M層裏面一個個業務類所提供的成員方法來實現。也就是說C層是不需要知道也不應該知道和客戶端和服務器通信所使用的任何協議,以及數據報文格式,以及存儲方面的內容。這樣的好處是客戶端和服務器之間的通信協議,數據格式,以及本地存儲的變更都不會影響任何的應用整體框架,因爲提供給C層的接口不變,只需要升級和更新M層的代碼就可以了。比如說我們想將網絡請求庫從ASI換成AFN就只要在M層變化就可以了,整個C層和V層的代碼不變。下面是M層內部層次的定義圖:


詳見:論MVVM僞框架結構和MVC中M的實現機制

針對Model進一步優化代碼:

新增業務模型:

class NewsModel {
    private(set) var itemList = Array<News>()
    
    // MARK: -數據
    var count: Int {
        return itemList.count
    }
    
    func item(at index: Int) -> News {
        return itemList[index]
    }
    
    /// 添加新的數據 (如上拉加載更多)
    func append(newItems: [News]) {
        itemList.append(contentsOf: newItems)
    }
    
    // MARK: -網絡
    func fetchAllDatas(callback: @escaping(_ success: Bool, _ errMsg: String) -> ()) {
        // 這個請求 視情況可以再單獨封裝一層網絡層
        AF.request("https://c.3g.163.com/nc/article/list/T1467284926140/0-20.html").responseJSON { rsp in
            var result = true
            var msg = ""
            switch rsp.result {
            case .success(let json):
                print(json)
                let jsonData = JSON.init(json)
                let newsJsonArr = jsonData["T1467284926140"].arrayValue
                self.itemList = newsJsonArr.map {
                    let title = $0["title"].stringValue
                    let createTime = $0["ptime"].stringValue
                    let coverSrc = $0["imgsrc"].stringValue
                    return News(title: title, createTime: createTime, coverSrc: coverSrc)
                }
            case .failure(let err):
                print(err)
                result = false
                msg = err.localizedDescription
            }
            
            callback(result, msg)
        }
    }
    
    // 分頁請求
    func fetchPartDatas(page: Int, callback: @escaping(_ success: Bool, _ errMsg: String) -> ()) -> Void {
        // ....
    }
    
    // MARK: -本地存儲
    // .....
    
    // MARK: -弱業務
    func newsItemTitle(at index: Int) -> String {
        return self.item(at: index).title
    }
    
    func newsItemDate(at index: Int) -> String {
        let createTime = self.item(at: index).createTime
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
        let date = dateFormatter.date(from: createTime)
        return dateFormatter.string(from: date ?? Date.now)
    }
    
    func newsItemCoverUrl(at index: Int) -> URL? {
        let urlSrc = self.item(at: index).coverSrc
        return URL(string: urlSrc.replacingOccurrences(of: "http:", with: "https:"))
    }
}

ViewController相關業務代碼遷移到Model層:

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(tableView)
        view.addSubview(activityIndicator)
        
        activityIndicator.startAnimating()
        newsModel.fetchAllDatas { success, errMsg in
            self.activityIndicator.stopAnimating()
            if success {
                self.tableView.reloadData()
            } else {
                // 錯誤處理
            }
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell: NewsTableViewCell
        if let rs = tableView.dequeueReusableCell(withIdentifier: cellId) as? NewsTableViewCell {
            cell = rs
        } else {
            cell = NewsTableViewCell(style: .subtitle, reuseIdentifier: cellId)
        }
        
        let index = indexPath.row
        cell.titleLabel.text = newsModel.newsItemTitle(at: index)
        cell.dateLabel.text = newsModel.newsItemDate(at: index)
        cell.coverImageView.kf.setImage(with: newsModel.newsItemCoverUrl(at: index))
        
        return cell
    }

�優化後的代碼,原先的數據News,還是隻保留服務器返回的數據字段;News跟業務完全無關,它的數據可以交給任何一個能處理它數據的其他對象來完成業務。News是完全獨立的,複用性很高也容易維護;但這樣相關的數據加工都丟到了業務模型NewsModel中(或相關helper中),News的操作也會出現在各種地方;另外一種方式就是就是將相關的數據加工等弱業務交由數據對象自己處理,但是後續該數據對象重用性將降低,更改的代碼如下:

struct News {
    let title: String
    let createTime: String
    let coverSrc: String
    
    init(_ jsonData: JSON) {
        title = jsonData["title"].stringValue
        createTime = jsonData["ptime"].stringValue
        coverSrc = jsonData["imgsrc"].stringValue
    }
    
    var newsItemDate: String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
        let date = dateFormatter.date(from: createTime)
        return dateFormatter.string(from: date ?? Date.now)
    }
    
    var newsItemCoverUrl: URL? {
        return URL(string: coverSrc.replacingOccurrences(of: "http:", with: "https:"))
    }
}

它們各有優缺點,可以根據項目情況選擇;

單向數據流

如果我們的demo中新聞列表支持2種加載方式,全量拉取、分頁拉取;而且可以切換;對於分頁加載會編寫類似的代碼:

   // 上拉加載更多
    private func loadMore() {
        activityIndicator.startAnimating()
        newsModel.fetchPartDatas { success, errMsg in
            self.activityIndicator.stopAnimating()
            if success {
                self.tableView.reloadData()
            } else {
                // 錯誤處理
            }
        }
    }

可以發現,我們編寫了同樣的控制activityIndicator加載、tableView刷新的邏輯;也就是我們更改了數據後,仍需要手動的維護數據改動後帶來的UI更新;如果後續還有更改數據的操作(如刪除一條、增加一條數據)等,還得繼續類似的代碼;這樣一來重複代碼過多,二來容易出錯;
按照MVC架構的M和C的關係,Model數據更改後應該通過KVO或Notification的方式通知Controller更改View;從而實現 操作-->更改數據、更改UI的流程 轉變爲 操作 --> 更改數據 --> 更改UI的單向數據流;

Swift中用屬性觀測器代替KVO實現:

// Model層 監聽數據變化並反饋給Controller
class NewsModel {
    private var itemList: Array<News> = [] {
        didSet {
            self.dataOnChanged?(())
        }
    }
    private var isLoading: Bool = false {
        didSet {
            self.loadingChanged?(isLoading)
        }
    }
    
    var dataOnChanged: ChangedBlock<Void>?
    var dataOnError: ChangedBlock<Error>?
    var loadingChanged: ChangedBlock<Bool>?
....
// 加載數據後不用再閉包回調
    func fetchAllDatas() {
        isLoading = true
        AF.request("https://c.3g.163.com/nc/article/list/T1467284926140/0-20.html").responseJSON { rsp in
            self.isLoading = false
            switch rsp.result {
            case .success(let json):
                let jsonData = JSON.init(json)
                let newsJsonArr = jsonData["T1467284926140"].arrayValue
                self.itemList = newsJsonArr.map { return News($0) }
            case .failure(let err):
                self.dataOnError?(err)
            }
        }
    }
....

Controller中綁定對應事件,更新UI

// NewsListViewController
        newsModel.dataOnChanged = { [weak self] _ in
            self?.tableView.reloadData()
        }
        
        newsModel.dataOnError = { [weak self] err in
            // 錯誤處理
        }
        
        newsModel.loadingChanged = { [weak self] loading in
            if loading {
                self?.activityIndicator.startAnimating()
            } else {
                self?.activityIndicator.stopAnimating()
            }
        }

然後,所有請求數據、更改數據的代碼就非常簡單了:加載框、tableview刷新都自動完成;

// 拉取全量數據
     newsModel.fetchAllDatas()

// 分頁拉取數據
      newsModel.fetchPartDatas()

假如現在有新需求,再增加一個功能:
點擊具體某條新聞時,更新新聞的閱讀量並更新對應顯示;
基於上面實現的單數據流,增加的代碼將異常簡單;
數據Model、View增加對應的數據段和UI控件;具體邏輯全部可以使用業務模型完成;

// NewsModel 代碼
    // 增加更新閱讀量的請求(模擬)
    func addReadCount(index: Int) {
        isLoading = true
        AF.request("https://www.baidu.com").response { rsp in
            self.isLoading = false
            switch rsp.result {
            case .success:
                var data = self.item(at: index)
                data.readCount += 1
                self.editData(at: index, newData: data)
            case .failure(let err):
                self.dataOnError?(err)
            }
        }
    }

    // 請求成功後,數據模型更改 (將觸發屬性觀察器)
    func editData(at index: Int, newData: News) {
        itemList[index] = newData
    }

// NewsListViewController代碼
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        newsModel.addReadCount(index: indexPath.row)
    }

ps: 這裏並不完美,因爲只更改了一行數據,但是也是觸發了dataOnChanged回調導致整個tableView全部刷新了;嚴謹的做法應該是,修改了哪行數據只刷新對應行的cell;具體做法可以將dataOnChanged回調細分,加一個返回參數表示是全部刷新、更新行刷新、新增行刷新、刪除行刷新等;具體可以參考下方喵神的文章;

View是否依賴Model

MVC框架圖中,View和Model是完全隔離的,它們間所有的交互都由Controller協調完成;
但實際開發中,當View的控件比較多,每個控件都需要配置的時候,Controller中相關賦值代碼會特別長;還有一點,當這個View在其他Controller重用時(綁定的數據模型一樣的前提下),又需要重新寫一樣很長的一代碼;爲了開發方便同時減少Controller的代碼量,大部分人會將Model直接丟給View,即View依賴於Model,然後內部配置控件數據;

// NewsTableViewCell
    func configData(item: News) {
        titleLabel.text = item.title
        dateLabel.text = ...
        coverImageView.kf.setImage ...
    }

// NewsListViewController
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       ....
       cell.configData(item)
        
       return cell
    }

這種做法無可厚非,本身開發這件事就是靈活變通;View依賴Model的優點分析了,但View的重用性,測試性都大大降低了;因爲View依賴了具體的Model,其他模塊需要重用View的或測試View的,需要額外配置出一個對應的具體Model;如何決策?同樣具體問題具體分析,如果View比較特別只會在一個特定業務模塊下使用,那綁定具體的Model益處更大;反之,就要考慮View和Model隔離;其實還有一種更優的方式,就是View依賴於抽象而不是具體類;在iOS中,可以定義一個協議,協議定義需要提供給View數據的接口,需要綁定View的Model實現協議相關接口提供數據;

基於面向協議MVP模式下的軟件設計-(iOS篇) 這篇文章就着重講了面向協議編程,有興趣的可以看看;

隨着不斷的優化,這個MVC架構其實就已經有了MVP、MVVM架構的雛形;MVP、MVVM本身也就是在MVC的基礎上優化了,只不過它們形成了一套自有的規範。從本質上,還是可以將他們稱爲MVC;
有了MVC的基礎,接下來就簡單聊聊MVP、MVVM

MVP

MVP(Model-View-Presenter),是MVC架構的一個演化版本。是基於MVC誤用的情況下的優化版;MVC誤用上面已講解的很清楚了,MVP也是將業務邏輯和業務展示分離,它創建了一個視圖的抽象也就是Presenter層,而視圖就是P層的渲染結果。P層中包含所有的視圖渲染需要的數據如text、color、組件是否啓用(enable),除此之外還會將一些方法暴露給視圖用於某些事件的響應。MVP並不是去掉了Controller,ViewController和View都合併歸爲View層,準確點說它應該叫MVCP;

目前常見的MVP架構模式其實都是它的變種:Passive ViewSupervising Controller
這裏這針對Passive View來分析;

Passive View(被動視圖):View層是被動的,其任何狀態的更新都交由Presenter處理;View持有Presenter,View通過Presenter的代理回調來改變自身的數據和狀態,View直接調用Presenter的接口來執行事件響應對應的業務邏輯;這種方式保證了Presenter的完全獨立,後續業務邏輯改動只需要更新Presenter而無需牽動View;但是帶來另一個問題是,View耦合了Presenter,和MVC的View耦合Model一樣可以使用協議方式優化;

各層職責

  • Model
    數據模型層
    單純的數據字段,負責接收數據、數據更改時通知Presenter。

  • View
    視圖層(View and/or ViewController)
    ViewController也屬於View層:負責View的生成,負責管理View的生命週期生成,實現View的代理和數據源;
    View: 監聽Presenter層的數據更新通知, 刷新頁面展示;將UI事件反饋給Presenter;

  • Presenter
    業務邏輯層
    相當於Model和View的中間人,類似與MVC中ViewController的功能,也即將之前Controller的部分工作單獨封裝一層成爲Presenter;負責實現View的事件處理邏輯,暴露相應的接口給View的事件調用;收到Model數據更新後,更新對應View;

對比可以發現,Passive View方式的MVP和上面最終版的MVC並沒有太大區別;無非是分層略有區別:將之前MVC的NewsModel更名NewsPresenter並歸爲Persenter層,其實就已經是最基本的MVP架構了;

// NewsListViewController
   let newsPresenter = NewsPresenter()  // 綁定Presenter
....

       newsPresenter.fetchAllDatas()
       newsPresenter.fetchPartDatas()
....

   func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
       newsPresenter.addReadCount(index: indexPath.row)
   }

Presenter細分

上述代碼中,我們將vc的self.view容器、tableView、loading當成一個view,綁定了唯一的Presenter (NewsPresenter);
實際上,當self.view由更多更復雜的view組成時,一個Presenter處理的業務也會更多、更雜亂;其實可以爲每一個獨立的view配置單獨的 Presenter;
下面我們將demo中的NewsTableViewCell都當做獨立的view,每個cell配置一個NewsCellPresenter;

之前由NewsPresenter處理的和cell有關的數據、業務全部挪到新的NewsCellPresenter,NewsPresenter的數據itemList保存爲NewsCellPresenter; 其他tablView列表數據、加載框數據仍不變;代碼如下:


// 通過代理 presenter通知cell更新
protocol NewsCellPresenterProtocol: AnyObject {
    func updateReadCount(presenter: NewsCellPresenter)
}

class NewsCellPresenter {
    weak var cell: NewsCellPresenterProtocol?
    
    private(set) var newsItem = News()
    
    // MARK: -弱業務
    func newsItemTitle() -> String {
        return self.newsItem.title
    }

    ....
    
    // MARK: -網絡 增加閱讀數的邏輯
    func addReadCount(callback: @escaping(_ success: Bool, _ errMsg: String) -> Void) {
        AF.request("https://www.baidu.com").response { rsp in
            var success = true
            var msg = ""
            switch rsp.result {
            case .success:
                self.newsItem.readCount += 1
                self.cell?.updateReadCount(presenter: self)
            case .failure(let err):
                success = false
                msg = err.localizedDescription
            }
            
            callback(success, msg)
        }
    }
}

NewsTableViewCell和NewsCellPresenter通信:

// NewsTableViewCell
    var presenter: NewsCellPresenter = NewsCellPresenter() {
        didSet {
            presenter.cell = self
            configData()
        }
    }

    func configData() {
        titleLabel.text = presenter.newsItemTitle()
        ....
    }

    func updateReadCount(presenter: NewsCellPresenter) {
        readCountLabel.text = presenter.newsItemReadText()
    }


// NewsListViewController
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        ....
        let cellPresenter = newsPresenter.cellPresenter(at: indexPath.row)
        cell.presenter = cellPresenter
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let cell = tableView.cellForRow(at: indexPath) as? NewsTableViewCell else { return }
        
        // loading框界面由 外層newsView的newsPresenter負責 回調回來交由其處理
        newsPresenter.isLoading = true
        cell.presenter.addReadCount { success, errMsg in
            self.newsPresenter.isLoading = false
        }
    }

MVVM

MVVM(Model — View — ViewModel),同MVP很相似,也是MVC架構的一個演化版本;

各層職責

從架構圖可以看出來,MVVM和MVP幾乎完全一樣;ViewModel層的作用其實就是和Presenter一樣,其他層也和MVP一致;
在原有MVP demo基礎上,改下類名基本上就是MVVM了:

MVVM其實是在MVP的基礎上發展、改良的;改良的地方,就是圖中和MVP中唯一不同的地方:
ViewModel和View之間加入了Binder層,可以實現雙向綁定;

關於數據綁定,其實在MVC演進版的單數據流中已經實現過;爲了區分MVP和MVVM,demo中MVP代碼中的更新閱讀數特意沒有綁定;可以分析下他的弊端:

點擊cell --> 通過presenter處理邏輯 --> 處理完畢更新數據,同時通過代理通知cell更新界面;
也就是每次都要把Present的狀態同步到View,當事件多起來的時候,這樣寫就很麻煩、且容易出錯了;
這時就需要bind機制了,當狀態、數據更改後自動更新對應的View;

還是一樣,通過屬性觀察器實現綁定:

class NewsCellViewModel {
    // 通過屬性觀測器 綁定
    var title = "" {
        didSet {
            
        }
    }
    var createTime = "" {
        didSet {
            
        }
    }
    var coverSrc = "" {
        didSet {
            
        }
    }
    var readCount = 0 {
        didSet {
            self.readCountBind?(newsItemReadText())
        }
    }
    
    // bind回調
    var readCountBind: ValueBinder<String>?

考慮到每個數據值都能單獨綁定,ViewModel中將item拆分,每個數據都監聽值更改;值更改後自動回調給View更新;

然後ViewController中綁定:

        let cellViewModel = newsViewModel.cellViewModel(at: indexPath.row)
        cell.viewModel = cellViewModel
        // 綁定
        cellViewModel.readCountBind = {[weak cell] countText in
            cell?.readCountLabel.text = countText
        }

ViewModel中只要更改數據即可:

    func addReadCount(callback: @escaping(_ success: Bool, _ errMsg: String) -> Void) {
        AF.request("https://www.baidu.com").response { rsp in
...
            case .success:
                self.readCount += 1
...
        }
    }

MVVM疑惑

  • 是否需要Controller?

大部分人覺得ViewModel做了Controller的事情,錯認爲MVVM不再需要Controller;
同MVP一樣,雖然它稱爲MVVM,但更準確來說應該是MVCVM;

Controller夾在View和ViewModel之間做的其中一個主要事情就是將View和ViewModel進行綁定。在邏輯上,Controller知道應當展示哪個View,Controller也知道應當使用哪個ViewModel,然而View和ViewModel它們之間是互相不知道的,所以Controller就負責控制他們的綁定關係;

  • MVVM一定需要RxSwift、ReactiveCocoa?

MVVM有數據綁定層,RxSwift、ReactiveCocoa等響應式框架很優美的實現了數據綁定;因此有部分人會覺得MVVM必須配合RxSwift、ReactiveCocoa使用;
事實上MVVM的關鍵是ViewModel !!!
在ViewModel層,我們可以通過delegate、block、kvo、notification等實現數據綁定;
RxSwift、ReactiveCocoa等響應式框架做數據綁定,簡潔優雅、有更加鬆散的綁定關係能夠降低ViewModel和View之間的耦合度;使用其可以更好體現MVVM的精髓,但並不表示其是MVVM必不可少的;
如果項目中轉向響應式編程,那MVVM+ RxSwift就是絕美配合;反過來,如果項目本身還是用的系統的那一套編程方式,只是爲了MVVM綁定而使用RxSwift等就是大材小用、得不償失了

關於MVVM+ RxSwift的實現,這裏就不做過多解析了,可能需要額外開一篇;


完整demo
(每個架構的類名一致,因此demo中想使用哪種架構就引用文件)


參考:
深入分析MVC、MVP、MVVM、VIPER
論MVVM僞框架結構和MVC中M的實現機制
iOS應用架構談 view層的組織和調用方案
關於 MVC 的一個常見的誤用
淺談 MVC、MVP 和 MVVM 架構模式
不再對 MVVM 感到絕望

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章