浅析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

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

各层职责

从架构图可以看出来,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;

MCVMVMV.gif

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 感到绝望

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容