前言
很早以前就想總結(jié)一下,iOS開發(fā)中常用的一些架構(gòu):MVC、MVP、MVVM;但是一直感覺自己沒有理解透徹,因為發(fā)現(xiàn)自己理解的和網(wǎng)上其他人的總是有出入;網(wǎng)上的眾說紛紜,仁者見仁智者見智;
隨著經(jīng)驗的增長,自己對于這些架構(gòu)的理解每次都有不同的收獲,漸漸的可能和最初了解的情況大相徑庭;
現(xiàn)在轉(zhuǎn)念一想,架構(gòu)這些事情并沒有絕對的對錯,也不會有什么標(biāo)準(zhǔn)答案;每個人都會結(jié)合自己的經(jīng)驗加以理解,實踐出最符合自己項目的架構(gòu);只要理解這些架構(gòu)的底層邏輯、運用其解決項目中的問題,那就不用在乎具體的招式是什么了;
下面就談?wù)勎覍VC/MVP/MVVM的理解
MVC
MVC (Model-View-Controller) 是蘋果推薦的架構(gòu)模式,也是其默認(rèn)使用的架構(gòu)模式。Apple官方MVC架構(gòu)定義如下:
圖示中簡單的列出了各層間的關(guān)系;結(jié)合iOS開發(fā)中實際場景,引用斯坦福的CS193p Paul老師的經(jīng)典MVC圖,更加清晰的說明各層間的通信:
各層職責(zé)
Model
業(yè)務(wù)模型層
Model封裝了應(yīng)用程序的數(shù)據(jù),也負(fù)責(zé)數(shù)據(jù)的獲取及數(shù)據(jù)的處理;
用戶在View中所進行的創(chuàng)建或修改數(shù)據(jù)的操作,通過Controller傳達出去直接更新Model;Model數(shù)據(jù)更改時,它通過KVO或NotificationCenter等方式通知Controller,Controller再更新相應(yīng)的View。View
視圖層
應(yīng)用程序中用戶可以看見的對象都屬于View層,對于iOS來說所有以UI開頭的類基本都是這一層;View層負(fù)責(zé)界面元素表達(包括動畫效果)及響應(yīng)用戶操作;
Controller收到Model的更新通知后,通過引用關(guān)系直接更新View;View層所需要的顯示數(shù)據(jù),Controller可以通過dataSource提供;View響應(yīng)事件后通過delegate或Target-Action等方式反饋給Controller處理;Controller
控制器層
它相當(dāng)于Model和View的中間人,負(fù)責(zé)Model和View的相互調(diào)配:當(dāng)Model數(shù)據(jù)更改時更新對應(yīng)的視圖,當(dāng)View更新或操作后更新對應(yīng)的數(shù)據(jù);
另外Model層和View層是沒有任何直接的關(guān)系的,它們之間的通信都由Controller完成;
MVC架構(gòu)的總的作用也體現(xiàn)出來了:
- 減少耦合性:各層分工明確,降低了相互的關(guān)聯(lián),方便維護
- 提高了代碼重用性:Model層和View層解耦了,方便重用
- 便于測試:在正確使用Model層的情況下,業(yè)務(wù)處理和View、Controller完全解耦,可以單獨測試業(yè)務(wù)邏輯;
MVC的困惑
但是在iOS實際開發(fā)中,慢慢的我們會發(fā)現(xiàn)大部分人寫的MVC已經(jīng)偏離了理想中的架構(gòu)設(shè)計;
一個明顯的特征就是ViewController層變得特別臃腫,代碼異常的多,形成了另一種MVC:Massive ViewController;
原因有很多,主要在于2點:
- Controller層包含View的顯示邏輯
- Model層誤解、誤用
下面通過一個demo來分析:MVC實現(xiàn)一個簡單的新聞列表界面;
這樣的實現(xiàn)代碼,只要入門級就能實現(xiàn);
// model層 由服務(wù)器返回的數(shù)據(jù):標(biāo)題、時間、封面
struct News {
let title: String
let createTime: String
let coverSrc: String
}
// view層,顯示新聞數(shù)據(jù)
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的顯示邏輯
根據(jù)命名就可以知道,ViewController并不是單獨的Controller而已,Apple的Cocoa框架把View和Cotroller組合在一起,ViewController同時做了View和Controller的事情;這是它和典型的MVC的不同之處,嚴(yán)格意義上來說也算違背了MVC架構(gòu)的原則了;
實際上Cocoa中的MVC架構(gòu)如下:
Cocoa為何要這么設(shè)計?
在服務(wù)端開發(fā)領(lǐng)域,Controller做完自己的事情之后,就把所有關(guān)于View的工作交給了頁面渲染引擎去做,Controller不會去做任何關(guān)于View的事情,包括生成View。這些都由渲染引擎代勞了。這是一個區(qū)別,但其實服務(wù)端View的概念和Native應(yīng)用View的概念,真正的區(qū)別在于:從概念上嚴(yán)格劃分的話,服務(wù)端其實根本沒有View,拜HTTP協(xié)議所賜,我們平時所討論的View只是用于描述View的字符串(更實質(zhì)的應(yīng)該稱之為數(shù)據(jù)),真正的View是瀏覽器。
所以服務(wù)端只管生成對View的描述,至于對View的長相,UI事件監(jiān)聽和處理,都是瀏覽器負(fù)責(zé)生成和維護的。但是在Native這邊來看,原本屬于瀏覽器的任務(wù)也逃不掉要自己做。那么這件事情由誰來做最合適?蘋果給出的答案是:UIViewController。
鑒于蘋果在這一層做了很多艱苦卓絕的努力,讓iOS工程師們不必親自去實現(xiàn)這些內(nèi)容。而且,它把所有的功能都放在了UIView上,并且把UIView做成不光可以展示UI,還可以作為容器的一個對象。
看到這兒你明白了嗎?UIView的另一個身份其實是容器!UIViewController中自帶的那個view,它的主要任務(wù)就是作為一個容器
詳見 Casa Taloyum文章 iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案
在iOS開發(fā)中,View上面的事件都是回傳給ViewController,然后ViewController再另行調(diào)度,ViewController可以根據(jù)不同事件的產(chǎn)生去很方便地更改容器內(nèi)容;如demo中,tableView的刷新,加載提示activityIndicator的顯示與否;
還有常用的,切換至無網(wǎng)絡(luò)、無數(shù)據(jù)頁面等等;
實際上iOS的MVC中Controller的職責(zé)會多幾個:
- 負(fù)責(zé)View的生成
- 負(fù)責(zé)管理View的生命周期
因此,Cocoa針對MVC這樣的處理是非常合適的;但是對于復(fù)雜界面,ViewController容易出現(xiàn)代碼膨脹,管理過多的狀態(tài),響應(yīng)代碼和邏輯代碼混淆一起,這將導(dǎo)致代碼很難維護,很難測試;
關(guān)于這種原因?qū)е碌腣iewController臃腫,業(yè)界也有一套解決方案;
ViewController有一個self.view的視圖容器,那么我們也可以把ViewController看成一個管理各個View的容器;簡單來說就是:將一個原業(yè)務(wù)ViewController拆分成容器coordinate vc和對應(yīng)的業(yè)務(wù)child vc來協(xié)調(diào)工作;之前的vc有多個頁面的,這時就拆分成多個child vc;
- child vc負(fù)責(zé)view的生成、響應(yīng)view的事件,管理自己view的生命周期;
- coordinate vc負(fù)責(zé)創(chuàng)建child vc,將child vc的視圖添加到自己的self.view容器上;同時管理view的生命周期、控制child vc獲取數(shù)據(jù)等操作;
這里不再單獨寫demo演示,大家只要類比UITabBarController就明白啥意思了。
這種方案實質(zhì)上就是將原先臃腫的vc平攤到每個child vc了,的確也是一種優(yōu)化方式,但是如果頁面邏輯比較多的情況child vc也容易出現(xiàn)一樣的問題,那還得將child vc再拆分下去,而且vc多了也容易出現(xiàn)協(xié)調(diào)的代碼變多、復(fù)雜;是否使用這種方案,需要結(jié)合自己項目具體情況決定;
Model層誤解、誤用
如果說上面這個因素是因為Apple本身設(shè)計引起,那 Model層誤解、誤用就完全是開發(fā)人員自己的原因了;
大部分人將Model理解成:只是單獨的數(shù)據(jù)模型;
如上面demo中News
對象,只有和服務(wù)器數(shù)據(jù)對應(yīng)的幾個字段的數(shù)據(jù)模型;如果只是數(shù)據(jù)模型,它也稱不上是層;
另一個問題表現(xiàn)是:Controller里的var newsList = Array<News>()
,我們等于把Model放到了Controller里,Model無法與Controlle 進行有效的通訊 (MVC圖中的Notification & KVO 部分)
實際上Model層正確定義是業(yè)務(wù)模型層,也就是所有業(yè)務(wù)數(shù)據(jù)和業(yè)務(wù)邏輯都應(yīng)該定義在Model層里面。
由于將Model層只是當(dāng)成了數(shù)據(jù)模型,導(dǎo)致了業(yè)務(wù)數(shù)據(jù)、邏輯在MVC架構(gòu)下無處安放;最終這些代碼還是堆砌到Controller層了;
Model層的正確設(shè)計:
M層要完成對業(yè)務(wù)邏輯實現(xiàn)的封裝,一般業(yè)務(wù)邏輯最多的是涉及到客戶端和服務(wù)器之間的業(yè)務(wù)交互。M層里面要完成對使用的網(wǎng)絡(luò)協(xié)議(HTTP, TCP,其他)、和服務(wù)器之間交互的數(shù)據(jù)格式(XML, JSON,其他)、本地緩存和數(shù)據(jù)庫存儲(COREDATA, SQLITE,其他)等所有業(yè)務(wù)細(xì)節(jié)的封裝,而且這些東西都不能暴露給C層。所有供C層調(diào)用的都是M層里面一個個業(yè)務(wù)類所提供的成員方法來實現(xiàn)。也就是說C層是不需要知道也不應(yīng)該知道和客戶端和服務(wù)器通信所使用的任何協(xié)議,以及數(shù)據(jù)報文格式,以及存儲方面的內(nèi)容。這樣的好處是客戶端和服務(wù)器之間的通信協(xié)議,數(shù)據(jù)格式,以及本地存儲的變更都不會影響任何的應(yīng)用整體框架,因為提供給C層的接口不變,只需要升級和更新M層的代碼就可以了。比如說我們想將網(wǎng)絡(luò)請求庫從ASI換成AFN就只要在M層變化就可以了,整個C層和V層的代碼不變。下面是M層內(nèi)部層次的定義圖:
詳見:論MVVM偽框架結(jié)構(gòu)和MVC中M的實現(xiàn)機制
針對Model進一步優(yōu)化代碼:
新增業(yè)務(wù)模型:
class NewsModel {
private(set) var itemList = Array<News>()
// MARK: -數(shù)據(jù)
var count: Int {
return itemList.count
}
func item(at index: Int) -> News {
return itemList[index]
}
/// 添加新的數(shù)據(jù) (如上拉加載更多)
func append(newItems: [News]) {
itemList.append(contentsOf: newItems)
}
// MARK: -網(wǎng)絡(luò)
func fetchAllDatas(callback: @escaping(_ success: Bool, _ errMsg: String) -> ()) {
// 這個請求 視情況可以再單獨封裝一層網(wǎng)絡(luò)層
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: -弱業(yè)務(wù)
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相關(guān)業(yè)務(wù)代碼遷移到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
}
?優(yōu)化后的代碼,原先的數(shù)據(jù)News,還是只保留服務(wù)器返回的數(shù)據(jù)字段;News跟業(yè)務(wù)完全無關(guān),它的數(shù)據(jù)可以交給任何一個能處理它數(shù)據(jù)的其他對象來完成業(yè)務(wù)。News是完全獨立的,復(fù)用性很高也容易維護;但這樣相關(guān)的數(shù)據(jù)加工都丟到了業(yè)務(wù)模型NewsModel中(或相關(guān)helper中),News的操作也會出現(xiàn)在各種地方;另外一種方式就是就是將相關(guān)的數(shù)據(jù)加工等弱業(yè)務(wù)交由數(shù)據(jù)對象自己處理,但是后續(xù)該數(shù)據(jù)對象重用性將降低,更改的代碼如下:
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:"))
}
}
它們各有優(yōu)缺點,可以根據(jù)項目情況選擇;
單向數(shù)據(jù)流
如果我們的demo中新聞列表支持2種加載方式,全量拉取、分頁拉取;而且可以切換;對于分頁加載會編寫類似的代碼:
// 上拉加載更多
private func loadMore() {
activityIndicator.startAnimating()
newsModel.fetchPartDatas { success, errMsg in
self.activityIndicator.stopAnimating()
if success {
self.tableView.reloadData()
} else {
// 錯誤處理
}
}
}
可以發(fā)現(xiàn),我們編寫了同樣的控制activityIndicator加載、tableView刷新的邏輯;也就是我們更改了數(shù)據(jù)后,仍需要手動的維護數(shù)據(jù)改動后帶來的UI更新;如果后續(xù)還有更改數(shù)據(jù)的操作(如刪除一條、增加一條數(shù)據(jù))等,還得繼續(xù)類似的代碼;這樣一來重復(fù)代碼過多,二來容易出錯;
按照MVC架構(gòu)的M和C的關(guān)系,Model數(shù)據(jù)更改后應(yīng)該通過KVO或Notification的方式通知Controller更改View;從而實現(xiàn) 操作-->更改數(shù)據(jù)、更改UI
的流程 轉(zhuǎn)變?yōu)?操作 --> 更改數(shù)據(jù) --> 更改UI
的單向數(shù)據(jù)流;
Swift中用屬性觀測器代替KVO實現(xiàn):
// Model層 監(jiān)聽數(shù)據(jù)變化并反饋給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>?
....
// 加載數(shù)據(jù)后不用再閉包回調(diào)
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中綁定對應(yīng)事件,更新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()
}
}
然后,所有請求數(shù)據(jù)、更改數(shù)據(jù)的代碼就非常簡單了:加載框、tableview刷新都自動完成;
// 拉取全量數(shù)據(jù)
newsModel.fetchAllDatas()
// 分頁拉取數(shù)據(jù)
newsModel.fetchPartDatas()
假如現(xiàn)在有新需求,再增加一個功能:
點擊具體某條新聞時,更新新聞的閱讀量并更新對應(yīng)顯示;
基于上面實現(xiàn)的單數(shù)據(jù)流,增加的代碼將異常簡單;
數(shù)據(jù)Model、View增加對應(yīng)的數(shù)據(jù)段和UI控件;具體邏輯全部可以使用業(yè)務(wù)模型完成;
// 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)
}
}
}
// 請求成功后,數(shù)據(jù)模型更改 (將觸發(fā)屬性觀察器)
func editData(at index: Int, newData: News) {
itemList[index] = newData
}
// NewsListViewController代碼
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
newsModel.addReadCount(index: indexPath.row)
}
ps: 這里并不完美,因為只更改了一行數(shù)據(jù),但是也是觸發(fā)了dataOnChanged回調(diào)導(dǎo)致整個tableView全部刷新了;嚴(yán)謹(jǐn)?shù)淖龇☉?yīng)該是,修改了哪行數(shù)據(jù)只刷新對應(yīng)行的cell;具體做法可以將dataOnChanged回調(diào)細(xì)分,加一個返回參數(shù)表示是全部刷新、更新行刷新、新增行刷新、刪除行刷新等;具體可以參考下方喵神的文章;
View是否依賴Model
MVC框架圖中,View和Model是完全隔離的,它們間所有的交互都由Controller協(xié)調(diào)完成;
但實際開發(fā)中,當(dāng)View的控件比較多,每個控件都需要配置的時候,Controller中相關(guān)賦值代碼會特別長;還有一點,當(dāng)這個View在其他Controller重用時(綁定的數(shù)據(jù)模型一樣的前提下),又需要重新寫一樣很長的一代碼;為了開發(fā)方便同時減少Controller的代碼量,大部分人會將Model直接丟給View,即View依賴于Model,然后內(nèi)部配置控件數(shù)據(jù);
// 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
}
這種做法無可厚非,本身開發(fā)這件事就是靈活變通;View依賴Model的優(yōu)點分析了,但View的重用性,測試性都大大降低了;因為View依賴了具體的Model,其他模塊需要重用View的或測試View的,需要額外配置出一個對應(yīng)的具體Model;如何決策?同樣具體問題具體分析,如果View比較特別只會在一個特定業(yè)務(wù)模塊下使用,那綁定具體的Model益處更大;反之,就要考慮View和Model隔離;其實還有一種更優(yōu)的方式,就是View依賴于抽象而不是具體類;在iOS中,可以定義一個協(xié)議,協(xié)議定義需要提供給View數(shù)據(jù)的接口,需要綁定View的Model實現(xiàn)協(xié)議相關(guān)接口提供數(shù)據(jù);
基于面向協(xié)議MVP模式下的軟件設(shè)計-(iOS篇) 這篇文章就著重講了面向協(xié)議編程,有興趣的可以看看;
隨著不斷的優(yōu)化,這個MVC架構(gòu)其實就已經(jīng)有了MVP、MVVM架構(gòu)的雛形;MVP、MVVM本身也就是在MVC的基礎(chǔ)上優(yōu)化了,只不過它們形成了一套自有的規(guī)范。從本質(zhì)上,還是可以將他們稱為MVC;
有了MVC的基礎(chǔ),接下來就簡單聊聊MVP、MVVM
MVP
MVP(Model-View-Presenter),是MVC架構(gòu)的一個演化版本。是基于MVC誤用的情況下的優(yōu)化版;MVC誤用上面已講解的很清楚了,MVP也是將業(yè)務(wù)邏輯和業(yè)務(wù)展示分離,它創(chuàng)建了一個視圖的抽象也就是Presenter層,而視圖就是P層的渲染結(jié)果。P層中包含所有的視圖渲染需要的數(shù)據(jù)如text、color、組件是否啟用(enable),除此之外還會將一些方法暴露給視圖用于某些事件的響應(yīng)。MVP并不是去掉了Controller,ViewController和View都合并歸為View層,準(zhǔn)確點說它應(yīng)該叫MVCP;
目前常見的MVP架構(gòu)模式其實都是它的變種:Passive View 和 Supervising Controller
這里這針對Passive View來分析;
Passive View(被動視圖):View層是被動的,其任何狀態(tài)的更新都交由Presenter處理;View持有Presenter,View通過Presenter的代理回調(diào)來改變自身的數(shù)據(jù)和狀態(tài),View直接調(diào)用Presenter的接口來執(zhí)行事件響應(yīng)對應(yīng)的業(yè)務(wù)邏輯;這種方式保證了Presenter的完全獨立,后續(xù)業(yè)務(wù)邏輯改動只需要更新Presenter而無需牽動View;但是帶來另一個問題是,View耦合了Presenter,和MVC的View耦合Model一樣可以使用協(xié)議方式優(yōu)化;
各層職責(zé)
Model
數(shù)據(jù)模型層
單純的數(shù)據(jù)字段,負(fù)責(zé)接收數(shù)據(jù)、數(shù)據(jù)更改時通知Presenter。View
視圖層(View and/or ViewController)
ViewController也屬于View層:負(fù)責(zé)View的生成,負(fù)責(zé)管理View的生命周期生成,實現(xiàn)View的代理和數(shù)據(jù)源;
View: 監(jiān)聽Presenter層的數(shù)據(jù)更新通知, 刷新頁面展示;將UI事件反饋給Presenter;Presenter
業(yè)務(wù)邏輯層
相當(dāng)于Model和View的中間人,類似與MVC中ViewController的功能,也即將之前Controller的部分工作單獨封裝一層成為Presenter;負(fù)責(zé)實現(xiàn)View的事件處理邏輯,暴露相應(yīng)的接口給View的事件調(diào)用;收到Model數(shù)據(jù)更新后,更新對應(yīng)View;
對比可以發(fā)現(xiàn),Passive View方式的MVP和上面最終版的MVC并沒有太大區(qū)別;無非是分層略有區(qū)別:將之前MVC的NewsModel更名NewsPresenter并歸為Persenter層,其實就已經(jīng)是最基本的MVP架構(gòu)了;
// NewsListViewController
let newsPresenter = NewsPresenter() // 綁定Presenter
....
newsPresenter.fetchAllDatas()
newsPresenter.fetchPartDatas()
....
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
newsPresenter.addReadCount(index: indexPath.row)
}
Presenter細(xì)分
上述代碼中,我們將vc的self.view容器、tableView、loading當(dāng)成一個view,綁定了唯一的Presenter (NewsPresenter);
實際上,當(dāng)self.view由更多更復(fù)雜的view組成時,一個Presenter處理的業(yè)務(wù)也會更多、更雜亂;其實可以為每一個獨立的view配置單獨的 Presenter;
下面我們將demo中的NewsTableViewCell都當(dāng)做獨立的view,每個cell配置一個NewsCellPresenter;
之前由NewsPresenter處理的和cell有關(guān)的數(shù)據(jù)、業(yè)務(wù)全部挪到新的NewsCellPresenter,NewsPresenter的數(shù)據(jù)itemList保存為NewsCellPresenter; 其他tablView列表數(shù)據(jù)、加載框數(shù)據(jù)仍不變;代碼如下:
// 通過代理 presenter通知cell更新
protocol NewsCellPresenterProtocol: AnyObject {
func updateReadCount(presenter: NewsCellPresenter)
}
class NewsCellPresenter {
weak var cell: NewsCellPresenterProtocol?
private(set) var newsItem = News()
// MARK: -弱業(yè)務(wù)
func newsItemTitle() -> String {
return self.newsItem.title
}
....
// MARK: -網(wǎng)絡(luò) 增加閱讀數(shù)的邏輯
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負(fù)責(zé) 回調(diào)回來交由其處理
newsPresenter.isLoading = true
cell.presenter.addReadCount { success, errMsg in
self.newsPresenter.isLoading = false
}
}
MVVM
MVVM(Model?—?View?—?ViewModel),同MVP很相似,也是MVC架構(gòu)的一個演化版本;
各層職責(zé)
從架構(gòu)圖可以看出來,MVVM和MVP幾乎完全一樣;ViewModel層的作用其實就是和Presenter一樣,其他層也和MVP一致;
在原有MVP demo基礎(chǔ)上,改下類名基本上就是MVVM了:
MVVM其實是在MVP的基礎(chǔ)上發(fā)展、改良的;改良的地方,就是圖中和MVP中唯一不同的地方:
ViewModel和View之間加入了Binder層,可以實現(xiàn)雙向綁定;
關(guān)于數(shù)據(jù)綁定,其實在MVC演進版的單數(shù)據(jù)流
中已經(jīng)實現(xiàn)過;為了區(qū)分MVP和MVVM,demo中MVP代碼中的更新閱讀數(shù)特意沒有綁定;可以分析下他的弊端:
點擊cell --> 通過presenter處理邏輯 --> 處理完畢更新數(shù)據(jù),同時通過代理通知cell更新界面;
也就是每次都要把Present的狀態(tài)同步到View,當(dāng)事件多起來的時候,這樣寫就很麻煩、且容易出錯了;
這時就需要bind機制了,當(dāng)狀態(tài)、數(shù)據(jù)更改后自動更新對應(yīng)的View;
還是一樣,通過屬性觀察器實現(xiàn)綁定:
class NewsCellViewModel {
// 通過屬性觀測器 綁定
var title = "" {
didSet {
}
}
var createTime = "" {
didSet {
}
}
var coverSrc = "" {
didSet {
}
}
var readCount = 0 {
didSet {
self.readCountBind?(newsItemReadText())
}
}
// bind回調(diào)
var readCountBind: ValueBinder<String>?
考慮到每個數(shù)據(jù)值都能單獨綁定,ViewModel中將item拆分,每個數(shù)據(jù)都監(jiān)聽值更改;值更改后自動回調(diào)給View更新;
然后ViewController中綁定:
let cellViewModel = newsViewModel.cellViewModel(at: indexPath.row)
cell.viewModel = cellViewModel
// 綁定
cellViewModel.readCountBind = {[weak cell] countText in
cell?.readCountLabel.text = countText
}
ViewModel中只要更改數(shù)據(jù)即可:
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的事情,錯認(rèn)為MVVM不再需要Controller;
同MVP一樣,雖然它稱為MVVM,但更準(zhǔn)確來說應(yīng)該是MVCVM;
Controller夾在View和ViewModel之間做的其中一個主要事情就是將View和ViewModel進行綁定。在邏輯上,Controller知道應(yīng)當(dāng)展示哪個View,Controller也知道應(yīng)當(dāng)使用哪個ViewModel,然而View和ViewModel它們之間是互相不知道的,所以Controller就負(fù)責(zé)控制他們的綁定關(guān)系;
- MVVM一定需要RxSwift、ReactiveCocoa?
MVVM有數(shù)據(jù)綁定層,RxSwift、ReactiveCocoa等響應(yīng)式框架很優(yōu)美的實現(xiàn)了數(shù)據(jù)綁定;因此有部分人會覺得MVVM必須配合RxSwift、ReactiveCocoa使用;
事實上MVVM的關(guān)鍵是ViewModel !!!
在ViewModel層,我們可以通過delegate、block、kvo、notification等實現(xiàn)數(shù)據(jù)綁定;
RxSwift、ReactiveCocoa等響應(yīng)式框架做數(shù)據(jù)綁定,簡潔優(yōu)雅、有更加松散的綁定關(guān)系能夠降低ViewModel和View之間的耦合度;使用其可以更好體現(xiàn)MVVM的精髓,但并不表示其是MVVM必不可少的;
如果項目中轉(zhuǎn)向響應(yīng)式編程,那MVVM+ RxSwift就是絕美配合;反過來,如果項目本身還是用的系統(tǒng)的那一套編程方式,只是為了MVVM綁定而使用RxSwift等就是大材小用、得不償失了
關(guān)于MVVM+ RxSwift的實現(xiàn),這里就不做過多解析了,可能需要額外開一篇;
完整demo
(每個架構(gòu)的類名一致,因此demo中想使用哪種架構(gòu)就引用文件)
參考:
深入分析MVC、MVP、MVVM、VIPER
論MVVM偽框架結(jié)構(gòu)和MVC中M的實現(xiàn)機制
iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案
關(guān)于 MVC 的一個常見的誤用
淺談 MVC、MVP 和 MVVM 架構(gòu)模式
不再對 MVVM 感到絕望