摘要
本系列文章将详细分析iOS的MVVMR
架构模式,并基于Swift的响应式、函数式编程框架RxSwift
提供相应的实现。
系列共分为两个部分:
- MVVMR架构的思想、基本原理及其初步实现
- 架构中某些细节部分的实现封装以及实战
这篇文章讲述的是第一部分的内容,我会先把架构的各个组成粗略地罗列出,然后再对它们进行详细的分析,最后结合代码进行实现。若文章中存在模糊或不合理的地方,还请各位包涵,也欢迎大家向我进行反馈。
前言
寒假期间,我和小组的伙伴们针对团队之前的一款还未上线的项目进行大重构,开发语言从Objective-C转到了Swift,作为组长,我负责了整个项目架构的搭建。在旧项目中,我是基于ReactiveCocoa
搭起了MVVM
这种架构模式。鉴于此架构模式优点颇多,并且在前面也已实践过,所以在新项目里我也依旧沿用了MVVM
,不过用于事件以及数据绑定的框架就改用了RxSwift
。关于RxSwift
,我在之前也写过一篇文章: RxSwift进阶与实战 ,这篇文章的实战部分也提供了一个简单的MVVM
架构模式实现。
分析下现在移动端开发比较热门的几款架构模式:MVC
, MVP
, MVVM
, VIPER
,除去最经典的模式MVC
外,其余的模式究其根本,其实都是从MVC
衍变而来,并且都是针对其中的Controller
层进行分层再细化,而本文所针对的MVVM
架构模式,原理上它为了减轻MVC
架构中Controller
层的业务负担,将Controller
再细分为两个部分,一部分跟原本的View
层合并在一起形成新的View
层,另一部分就变成了ViewModel
层。(这里所说的Controller并不能指代iOS中的UIViewController)对于我来说MVP
跟MVVM
极其相似,如果偏要让我说出它们的不同,我会主观地认为MVP
中Presentation
做的事情只是数据与事件的解析转换等业务逻辑,而MVVM
的ViewModel
中除了这些业务逻辑外,里面还可以存有某些状态变量,像在RAC
中,专门有一个宏用于状态变量与信号的绑定:RAC(viewModel, userToken) = userTokenSignal
,而RxSwift
我个人认为是倾向于流的转换,尽量避免出现状态变量。所以重构项目中有时候我会想: "我TM是在写MVVM还是MVP?!",算了,不要在意这些细节...
在上面我一直都在说MVVM
模式,但是文章的标题呈现的却是MVVMR
,R其实是我“自作主张”增添的Router(路由器)
,这个设计在下面会说到。
接下来我就本此项目的架构,向大家进行详细的分析。
MVVMR基本蓝图
首先给大家看两张示意图:
基本构成 事件、数据流转换
这两张图分别表示了MVVMR
架构中的基本构成要素和流转换示意。
在图从,我们可以看到整个架构的组成要素分别是:View(视图)
、ViewModel(视图模型)
、Input(输入)
、Output(输出)
、Router(路由器)
、UnitCase(单位实例)
, 其中,View
与Input
同组成View
层,ViewModel
与Output
组成ViewModel
层,UnitCase
为一个简单的数据结构,用于保存View
和ViewModel
的关系,用于后期绑定器对它们相互间进行绑定,这个在后面会详细说到。
接下来有两方面需要提及:
架构目的
前面说到,传统的MVC
架构中,ViewController
由于要处理过多的业务逻辑以及对View
层的显示逻辑,会变得越来越复杂,最后将会成为一个重量级角色,在开发中容易乱了手脚,并且严重缺乏可维护性。MVVM
架构致力于减轻ViewController
层的负担,将一部分属于纯业务逻辑处理放到了ViewModel
中,而对于View
的显示逻辑,如UIView布局渲染、动画等就一并归于View
层。
View : 视图的布局、渲染、动画、UIViewController的转场
ViewModel : 纯业务逻辑处理
Model : 提供数据,如网络请求数据,本地数据库、UserDefaults
有一点需要注意的是,因为MVVM
比起MVC
来说在层级的数量上有所增加,所以我们需要再从原来的基础上多维护了某些东西,这很容易造成架构中耦合度的上升,为了降低耦合,我在架构中引入了Input
以及Output
的概念,后面有详细的分析。
架构思想
整套架构围绕着的一个思想是: 事件与数据基于流的抽象
我们把事件(如用户的触发事件)以及数据(网络、本地数据)抽象成在一条在管道中流动的流,每一次的业务处理,都像是一个接入了这条管道中的流处理器,将流入的流转换加工,并输出处理过后的流。因为事件或数据可能会涉及多个不同的业务处理,所以在管道中也可以接入多个流处理器,让事件和数据在管道中流动的时候发生连锁反应。
在本架构中,
RxSwift
框架就是这条包裹着事件与数据流的管道。
各模块详解
接下来我就MVVMR架构中各重要模块的概念,结合iOS的实际开发来详细说明。
View
View
层做的东西都只是跟视图有关系,布局、渲染、动画等等,并不会接触与业务相关的内容。它汇总视图的触发事件或数据,构建出Input
传入ViewModel
,并接受ViewModel
传过来的Output
,刷新视图显示。
架构中,我把UIViewController
也归入了View
层中,因为个人觉得ViewController
与视图有着非常密切的关联,若要强制性分离职责,应该将ViewController
里面的所有业务逻辑抽离出来,让UIViewController
其只充当View
的一部分。
View
中还持有路由器Router
,用于视图的跳转。(接下来会说到)
ViewModel
说简单点,ViewModel
做的事情就只有一件: 转换,说复杂点,ViewModel
需要将View
传入的Input
中所有的事件数据流进行转换处理,最终将完成处理后的流放入Output
中传递给View
,所有的业务逻辑都是在这里进行实现,其中涉及到数据的请求(网络、本地)需要向Model
请求获取。
Model
模型层,数据提供者。提供网络请求数据,本地数据库、UserDefaults缓存数据,支持对数据进行解析处理。不过在实际项目中,Model
层并没有很明显地表示出来,我是将网络请求、JSON数据解析和本地缓存封装在一起构建出一个较为强大的“流转换器”,其也算是Model层的一部分。相关网络请求的封装我会在下一篇中谈到。
Input & Output
Input
和Output
其实是一个容器,里面装载着各种事件数据流,在View
与ViewModel
通信中起到传递的作用。
看到这里,可能有人会认为,View
与ViewModel
之间的相互通信较为简单,只需通过方法去调用即可,没必要又另外再构建多一个Input
和Output
。其实,在框架设计中,我构建了这两个东西,主要目的就是实现View
层与ViewModel
层的完全解耦。架构在工作的时候,View
以及ViewModel
各自维护着自己的运作,且它们之间不存在过多的耦合,即两层之间互不关注,也互不知道对方的实际情况,它们间的通信只依赖于Input
和Output
。这样,在开发以及后期的维护中,我们在对其中一层进行修改或重构时,另外一层可完全不需要改动。
在实际项目中,我使用的是Swift的struct(结构体)
去实现Input
和Output
,并将每个事件数据流作为结构体中的属性来持有。这样做的好处是每个事件数据流都能清晰明了地列举在代码中,通过观察Input
的属性,我们能知道View
能够产出多少种视图触发的事件数据流,通过观察Output
的属性,我们也得知View
最终要接收哪些更新视图的事件数据流。
这里总结下设计Input
与Output
的目的:
- 实现
View
与ViewModel
层之间的解耦 - 能够清晰罗列出各种事件数据流
Binder & UnitCase
首先来说下Binder(绑定器)
,它要做的事情就是将View
以及ViewModel
进行绑定。这里就抛出了一个问题: 我们为什么需要绑定?
我们知道,iOS应用是以页面为单元的,一个页面就是一套MVC
或MVVM
工作的结果。而普通的应用本身是拥有非常多的页面,若我们使用的架构模式是MVVM
,这就需要创建同等数量的若干套MVVM
,而MVVM
的构建需要将各层各模块联系绑定在一起。若每次我们在需要跳转到一个新页面时才去对架构进行绑定,这就增大了代码的复杂度以及冗余度,所以,我们需要一套机制,在我们需要呈现一个新页面时,自动帮我们将架构各层各模块进行绑定。在MVVMR
架构中,我使用的是Binder
来实现这种机制。
但是,在绑定时,我们必须要将View
跟ViewModel
一一对应起来,不可能说将一个页面的View
跟其他页面的ViewModel
进行绑定。所以,为了明确这种一一对应关系,我引入了UnitCase
,它是一个存有View
和ViewController
对应关系的容器。通过UnitCase
,Binder
就能正确绑定View
和ViewModel
层。
Router
路由器就是为了实现各页面之间的跳转。为什么架构中不直接使用iOS API提供的pushViewController
、presentViewController
等方法呢?这里考虑到两个问题:
- 页面需要创建后才能够进行跳转,而在创建页面的时候需要进行绑定。如果将页面绑定跟跳转封装在一起将带来较大的便利。
- 页面间需要传递事件和数据,通常的做法是使用方法传递(正向传递)或者代理模式(反向传递),这样做耦合度较大。需要一种实现页面间传递数据且耦合度较低的机制。
路由器就是解决以上问题的优雅方案,它与Binder(绑定器)
密切结合,并提供页面间数据传递的接口。所以,这篇文章所讲述的架构模式名为MVVMR(传统MVVM+Router)。
架构实现
下面就是"Show My Code"的时间,以下我会贴出初步实现MVVMR
架构的相关代码,对于一些更为细节的实现封装,我会在下一篇文章中谈到。
协议部分
Swift作为一门倾于“面向协议”编程范式的语言,编写的时候当然要更好地去发挥其协议的作用。
Input & Output Protocol
/// Input Output
protocol ViewToViewModelInput {
init(view: MVVMView)
}
protocol ViewModelToViewOutput {
init(viewModel: MVVMViewModel)
}
上面就是Input
和Output
的协议定义,从名字上可以很清晰地看出它们的作用,一个是从View
传递到ViewModel
,而另一个则是反过来传递。它们的构造都需要自身的发出者。
Provider Protocol
/// Provider
protocol ViewToViewModelInputProvider {
var inputType: ViewToViewModelInput.Type? { get }
func provideInput() -> ViewToViewModelInput?
}
extension ViewToViewModelInputProvider where Self: MVVMView {
func provideInput() -> ViewToViewModelInput? {
return self.inputType?.init(view: self)
}
}
protocol ViewModelToViewOutputProvider {
var outputType: ViewModelToViewOutput.Type? { get }
func provideOutput() -> ViewModelToViewOutput?
}
extension ViewModelToViewOutputProvider where Self: MVVMViewModel {
func provideOutput() -> ViewModelToViewOutput? {
return self.outputType?.init(viewModel: self)
}
}
Provider(提供者)
是针对Input
跟Output
的构建而设计的,意为Input
与Output
的提供者。每个提供者里具有一个元类类型的属性以及一个提供方法,而在分类中,提供方法已经帮我们去实现了。所以在实现提供者协议的时候,我们只需提供相应的Input
或Output
类型即可。
View 和 ViewModel 实现
View
以及ViewModel
的实现,我是拟好了两个抽象类:MVVMView
和MVVMViewModel
。
// MARK: - View & ViewModel
class MVVMView: UIViewController, ViewToViewModelInputProvider {
private let viewModelType: MVVMViewModel.Type?
private(set) var router: Router!
var viewModel: MVVMViewModel?
var inputType: ViewToViewModelInput.Type? { return nil }
var receive: Driver<Any>?
required init(_ viewModelType: MVVMViewModel.Type?) {
self.viewModelType = viewModelType
super.init(nibName: nil, bundle: nil)
self.router = Router(from: self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
if let viewModelType = self.viewModelType, let input = self.provideInput() {
self.viewModel = viewModelType.init(input: input)
if let output = self.viewModel!.provideOutput() {
rxDrive(viewModelOutput: output)
}
}
}
func rxDrive(viewModelOutput: ViewModelToViewOutput) { crash("抽象方法,在此进行绑定,此方法必须重写!") }
func provideCallBack() -> Driver<Any>? { return nil }
let disposeBag = DisposeBag()
}
class MVVMViewModel: NSObject, ViewModelToViewOutputProvider {
let input: ViewToViewModelInput
var outputType: ViewModelToViewOutput.Type? { return nil }
required init(input: ViewToViewModelInput) {
self.input = input
}
}
我们先看这两个抽象类的继承以及协议关系:MVVMView
继承的是UIViewController
,所以这时候UIViewController
作为View
层的一员,只负责视图的显示相关,并不会接触到业务逻辑的处理。MVVMViewModel
则简单地继承NSObject
。这两个抽象类都是实现了Provider
协议,担任了Input
和Output
的提供职责。在提供者协议实现元类型的属性时,我返回的是nil
,若继承抽象类的子类没有重写此属性,则告诉框架自己并没有提供了Input
或Output
,对此框架就取消对它们的绑定。
我们可以看到,MVVMView
对MVVMViewModel
进行了强引用,我们只需持有MVVMView
实例,就能依旧维持MVVMViewModel
的存在;并且,MVVMViewModel
在MVVMView
中的访问修饰为private,因此所有继承MVVMView
都抽象类都无法访问此属性,做到了两者的高度解耦。
这里在详细看回MVVMView
,其具有router(路由器)
属性,通过路由器,我们可以进行页面的跳转;另外有几个抽象的属性和方法:
- receive : 用于页面的通信,与
Router(路由器密切相关)
,当此页面收到上一个页面所传递进来的事件数据时,receive
就会赋入这些信息。在设计上,receive
的赋值紧接在MVVMView
的初始化之后,所以,我们不可以在重写的初始化方法中获取receive
的值。 - rxDrive(viewModelOutput:) : 这个方法就是用于将从
ViewModel
层出来的事件数据流驱动整个页面的显示,这方法中,我们可以通过传进来的参数Ouput
驱动视图的刷新显示、进行页面的跳转。这个方法也是ViewModel
层向View
层传递信息的唯一出口。 - provideCallBack() : 此方法是用于页面跳转中的反向数据传递,即将数据从本页面传到上一个页面,因为我们使用的是响应式的
RxSwift
框架,所以数据的反向传递就不需要使用到闭包或代理模式。在后面的Router
实现中会说到它的机制。 - disposeBag : 用于
RxSwift
资源的回收。
在MVVMView
的viewDidLoad()
方法中,我们进行MVVMView
和MVVMViewModel
的关联,如果MVVMView
和MVVMViewModel
没有提供Input
和Output
,则表明此时View
和ViewModel
层没有通信,所以也就不会调用rxDrive
方法了。
UnitCase
// MARK: - Unit
struct MVVMUnit {
let viewType: MVVMView.Type
let viewModelType: MVVMViewModel.Type
}
extension MVVMUnit: ExpressibleByArrayLiteral {
typealias Element = AnyClass
init(arrayLiteral elements: Element...) {
guard elements.count == 2 else { crash("单元初始化参数长度错误") }
guard let viewType = elements[0] as? MVVMView.Type else { crash("单元初始化参数类型错误") }
guard let viewModelType = elements[1] as? MVVMViewModel.Type else { crash("单元初始化参数类型错误") }
self.viewType = viewType
self.viewModelType = viewModelType
}
}
struct MVVMUnitCase: RawRepresentable {
typealias RawValue = MVVMUnit
let rawValue: MVVMUnit
init(rawValue: RawValue) {
self.rawValue = rawValue
}
}
上面的代码使用到了一个函数crash(_ message:)
,为断言函数,这里就不需要给出具体实现了。
我们先来看MVVMUnit
,它具有两个元类型的属性,分别代表一个页面中的MVVMView
和MVVMViewModel
类型,通过这种关系,绑定器就能正确地按照一一对应关系绑定MVVMView
和MVVMViewModel
。MVVMUnit
还实现了ExpressibleByArrayLiteral
,我们可以直接简便地通过数组字面量来初始化MVVMUnit
。
而MVVMUnitCase
则是对MVVMUnit
的再一次封装,其实现了RawRepresentable
协议,这样我们就能像使用枚举一样通过点.
语法来创建它。
使用的话我这里举个例子,加入现在我们的项目中需要用到两个页面,一个是主页面"main",一个是登录页面"login",它们都有对应的MVVMView
和MVVMViewModel
:MainMVVMView、MainMVVMViewModel
、LoginMVVMView、LoginMVVMViewModel
,我们则需要在MVVMUnitCase
中进行添加:
extension MVVMUnitCase {
static let main = MVVMUnitCase(rawValue: [MainMVVMView.self, MainMVVMViewModel.self])
static let simpleInfo = MVVMUnitCase(rawValue: [LoginMVVMView.self, LoginMVVMViewModel.self])
}
Binder
// Binder
struct MVVMBinder {
/// 根据标识符获取视图,会在背后做视图与视图模型的绑定
///
/// - Parameter identifier: 标识符
/// - Returns: 返回已经绑定好了的视图
static func obtainBindedView(_ unitCase: MVVMUnitCase) -> MVVMView {
let unit = unitCase.rawValue
let viewType = unit.viewType
let viewModelType = unit.viewModelType
let view = viewType.init(viewModelType)
return view
}
}
绑定器做的事情是对MVVMView
和MVVMViewModel
的绑定,它具有一个静态方法obtainBindedView(_ unitCase:)
在这个方法中我们需要传入一个MVVMUnitCase
的实例,然后绑定器会帮我们创建MVVMView
和MVVMViewModel
实例并进行绑定,最后将返回MVVMView
的实例,我们拿到这个实例就能进行页面的跳转。
Router
// MARK: - Router
enum RouterType {
case push(MVVMUnitCase)
case present(MVVMUnitCase)
case root(MVVMUnitCase)
case back
}
struct Router {
let from: MVVMView
init(from: MVVMView) {
self.from = from
}
func route(_ type: RouterType, send: Driver<Any>? = nil) -> Driver<Any>? {
switch type {
case let .push(unitCase):
let view = MVVMBinder.obtainBindedView(unitCase)
view.receive = send
from.navigationController?.pushViewController(view, animated: true)
return view.provideCallBack()
case let .present(unitCase):
let view = MVVMBinder.obtainBindedView(unitCase)
view.receive = send
from.present(view, animated: true, completion: nil)
return view.provideCallBack()
case let .root(unitCase):
let view = MVVMBinder.obtainBindedView(unitCase)
view.receive = send
UIApplication.shared.keyWindow?.rootViewController = view
return view.provideCallBack()
case .back:
if from.presentationController != nil {
from.dismiss(animated: true, completion: nil)
} else {
_ = from.navigationController?.popViewController(animated: true)
}
return nil
}
}
}
extension MVVMView {
func route(_ type: RouterType, send: Driver<Any>? = nil) -> Driver<Any>? {
return self.router.route(type, send: send)
}
}
常见的页面跳转为导航控制器的Push
、Pop
,模态的Present
、Dismiss
,UIWindow
的rootViewController(根视图控制器切换)
,我们把这些跳转作为一个枚举设计了RouterType
,其中back
则代表Pop
和Dismiss
,在使用RouterType
时,我们将目标的MVVMUnitCase
作为枚举的关联值传入。
对于路由器Router,我们需要使用一个MVVMView
来初始化它,代表跳转是从这个MVVMView
开始的。调用路由器中的route(_ type: , send:)
方法就能进行页面的跳转,其中,参数send
就是要传递到下一个页面的事件数据,而方法的返回值则为下一个页面反向传递过来的事件数据,通过MVVMView
的抽象方法provideCallBack()
。
我也为MVVMView
创建了一个扩展,在扩展中我们可以直接调用自身路由器的路由方法。
这里贴出个路由器的使用例子:
_ = view.route(.push(.login), send: Driver.just(userToken))
因为MVVMUnitCase
实现了RawRepresentable
协议,所以我们可以直接通过点语法来取得登录的Unit: .login
架构使用
到此,整套架构的实现就基本完成了,下面我们来结合RxSwift
来构建一个使用此套架构的Demo:
View 层
class DemoMVVMView: MVVMView {
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.mButton)
}
fileprivate lazy var mButton: UIButton = {
$0.frame = self.view.bounds
$0.setTitle("点击", for: .normal)
return $0
}(UIButton())
override var inputType: ViewToViewModelInput.Type? { return DemoInput.self }
override func rxDrive(viewModelOutput: ViewModelToViewOutput) {
let output = viewModelOutput as! DemoOutput
output.color.drive(self.rx.updateButtonBackgroundColor).addDisposableTo(self.disposeBag)
output.title.drive(self.rx.updateButtonTitle).addDisposableTo(self.disposeBag)
}
}
// MARK: - Reactive
extension Reactive where Base: DemoMVVMView {
var updateButtonTitle: AnyObserver<String> {
return UIBindingObserver<Base, String>(UIElement: base) { view, newTitle in
view.mButton.setTitle(newTitle, for: .normal)
}.asObserver()
}
var updateButtonBackgroundColor: AnyObserver<UIColor> {
return UIBindingObserver<Base, String>(UIElement: base) { view, newColor in
view.mButton.backgroundColor = newColor
}.asObserver()
}
}
// MARK: - Input
struct DemoInput: ViewToViewModelInput {
let refresh: Driver<()>
init(view: MVVMView) {
let view = view as! DemoMVVMView
self.refresh = view.mButton.rx.tap.asDriver()
}
}
可以看到,我们在DemoMVVMView
中添加了一个按钮,按钮的点击事件作为从View
层传入ViewModel
层的事件数据流,所以在DemoInput
中我们定义了按钮点击的刷新流。
在属于DemoMVVMView
的Reactive
分类中,里面的观察者代表DemoMVVMView
接收到ViewModel
层传来的事件数据流时进行的驱动操作,为更新按钮的标题和切换按钮的背景色。
我们重写inputType
属性以及rxDrive
方法,在inputType
属性中返回DemoInput
类型,在rxDrive
方法中将ViewModel
传过来的Output
驱动视图的刷新。
ViewModel 层
// MARK: - ViewModel
class DemoMVVMViewModel: MVVMViewModel {
override var outputType: ViewModelToViewOutput.Type? { DemoOutput.self }
}
struct DemoOutput: ViewModelToViewOutput {
let color: Driver<UIColor>
let title: Driver<String>
init(viewModel: MVVMViewModel) {
let viewModel = viewModel as! DemoMVVMViewModel
let input = viewModel.input as! DemoInput
self.color = input.refresh.map { _ in UIColor.orange }
self.title = input.refresh.map { _ in "数据已刷新" }
}
}
ViewModel
层相对较为简单,因为现在还未涉及网络请求操作或数据库操作,也没有进行某些业务逻辑的判断处理,所以DemoMVVMViewModel
中只有一个重写的OutputType
属性。在实际开发中,MVVMViewModel
中会持有某些临时的状态变量,或网络、本地数据库框架实例。
对于流的转换,发生在Output
的初始化方法中,可能有人会疑惑: “流的转换不是发生在MVVMViewModel
中的吗,为什么会在Output
的初始化方法中?” 在前面我说到,流的转换是在ViewModel
层发生的,而ViewModel
层是包含Output
跟MVVMViewModel
的,并不是说MVVMViewModel
名字的关系所以MVVMViewModel
就代表整个ViewModel
层。在此架构中,MVVMViewModel
主要是用于持有某些状态变量、某些服务模块和提供Output
类型的。
MVVMUnitCase
不要忘记在UnitCase
中对刚构建好的MVVM
进行配置:
extension MVVMUnitCase {
static let demo = MVVMUnitCase(rawValue: [DemoMVVMView.self, DemoMVVMViewModel.self])
}
在项目开发中,我们就可以在任意MVVMView
中进行向"Demo"页面的跳转了:
_ = self.route(.push(.demo))
上面展示的架构使用Demo较为简单,在下一篇文章中,我会结合封装完成后的网络请求框架,对MVVMR
架构进行一此较为大型的实战。
总结
这篇文章详细说明了我在项目中搭建出来的MVVMR
架构其思想、基本原理以及初步实现,为基于RxSwift的MVVMR架构系列文章的第一篇。若大家发现文章中的内容存在问题或者有更好的建议,欢迎向我反馈!
在下一篇文章中,我会对架构中的某些细节进行实现与封装,如基于Moya + Argo
的网络框架封装,并在后面使用架构进行实战使用。
网友评论
<Base, String> 应该是 <Base, UIColor>
var updateButtonBackgroundColor: AnyObserver<UIColor> {
return UIBindingObserver<Base, String>(UIElement: base) { view, newColor in
view.mButton.backgroundColor = newColor
}.asObserver()
}