iOS体验VIP架构
前言
对于一个设计比较简单的app来说,这个app对应只有几个页面,功能也不是很多的时候,这个app总的代码量比较少,我们使用 MVC(Model-View-Controller) 架构就能轻易的满足我们开发的需求,而且代码量较少也意味着对项目的维护是相对容易的。
然而,当一个app设计复杂,进行了许多版本的迭代更新,有几十个页面和各种复杂的功能,这个app代码量就变得很多,然而我们还是使用View和Controllern难以分离的 MVC 架构,我们在Controller所做的事情太多了,不管是视图展示的代码还是业务逻辑处理的代码统统都写到了Controller,导致这个部分的代码太多,难以测试和维护,对于中途接手这个项目的人来说简直是噩耗,于是 MVC 架构又被其他人笑话为 Massive-View-Controller。
于是聪明又懒惰的程序猿们想将一个Controller中的代码拆分出来,于是从MVC架构衍生了其他架构:MVP、MVVM、VIPER等等(这里就不介绍这些架构了,大家可以自行百度谷歌),而对于模块越细分,开发难度可能相对加大,但是对于测试维护来说就变得越简单。
VIP架构
在这篇文章,我将向大家介绍由Clean Swift提出 VIP(ViewController-Interactor-Presenter)架构,先来看看图示:
VIP核心三部分图示是这个架构最核心的三大部分,这三部分组成一个环形,每一部分都作为上一部分的output和下一部分的input(根据箭头所示方向的关系)。
接下来我就一一说明每个部分的作用:
ViewController
负责视图的展示
它作为Presenter的output,从Presenter获取的ViewModel
来对视图进行数据的赋值,ViewController本身不会对数据进行任何的处理,只会从ViewModel
中获取,ViewModel
给什么数据,它就展示什么数据。
同时它也作为Interactor的input,当用户与界面进行交互时,它就将这个事件和处理这个事件所需要的参数作为一个Request
提供给Interactor,让Interactor来处理这个交互事件。
Interactor
处理用户与界面交互时产生的事件
它作为ViewController的output,当ViewController与用户触发了交互事件,Interactor就会从ViewController提供过来的Request
中获取必要的参数来进行处理(比如网络请求、数据库查询),其实它本身是一个事件处理的管理者,它底下还有很多为它服务的工人(Worker),例如负者网络请求的工人、数据库查询的工人,这些工人才是真正处理事件的基本单位(后面会有更详细的图片来好好介绍)。
同时它也作为Presenter的input,当工人获取了数据之后,将结果封装成一个Response
然后提供给Presenter来处理。
Presenter
将数据处理成视图所要展示的内容
它作为Interactor的output,将Interactor提供的Response进行处理,有时我们只需要Response其中的一些数据,或者将数据排序什么的一些处理,都在这里进行,处理完后将数据封装成ViewModel。
同时它也作为ViewController的input,为ViewController展示视图时提供所必须的并且已经处理好的数据。
介绍完这个架构最主要的VIP部分,接下来我们来看看这个架构的全貌 架构全貌
比起上面的图,这张图片多了Worker、Models、Router。(图片做的很简陋,大家别吐槽...)
因此一个完整的VIP架构包含了:
- ViewContoller
- Interactor
- Presenter
- Worker
- Models
- Router
Worker
上面在介绍Interactor的时候也提到过了,它们是在Interactor里真正处理事件的基本单位,每个Worker有它们单一的职责。需要进行网络请求的任务,就调用负责网路请求的Worker,需要进行数据库更新或者获取的任务,就调用负责数据库处理的Worker,我们可以按事件类型来定义许多不同的Worker。Worker处理完后将处理回调给Interactor。
Models
当在ViewController中与用户产生交互事件时,从ViewController到Interactor再到Presentrt最后回到ViewController都会产生一系列的Request
、Response
和ViewModel
。而对于每个特定的事件,都有特定的Request
、Response
和ViewModel
,于是在Models中,我们会利用Swift独特命名空间的方式将每个事件定义一个命名空间,而这个事件的命名空间里包含了Request
、Response
和ViewModel
这三个Model
。
- Request:网络请求或数据库查询所需要的一些参数。
- Response:网络请求或数据库查询返回的一些数据。
- ViewModel:将Response处理成视图需要展示的数据。
Router
路由器,顾名思义就是用来进行页面跳转的。在Router里面,我们需要根据根据情况是否通过segue
跳转,或者通过Storyboard
或XIB
来创建,或则直接通过代码创建,并且对新控制器进行初始化赋值,最后进行跳转。对于新控制器的初始化赋值我们是通过Router里面的DataStore
属性来进行获取的,而作为这Router的DataStore
就是Interactor,因为页面跳转也属于用户交互行为,是一个跳转事件,在跳转前需要做跳转前的处理,处理完后,Router在从Interactor获取数据来将控制器初始化。
Demo体验
对于VIP架构每个部分已经介绍完了,让我们通过万能的登录Demo实际感受一下这个架构。
在这里我们主要是讲Login部分,DataBase是模拟数据库存取,Network是模拟登陆操作的网络请求,而Main模块是登陆成功后进行初始化控制器并跳转的演示所用到。整个架构的关系是基于Protocol实现的,并且以Protocol作为属性的类型(Swift面向协议发开)。我们通过点击登录按钮事件来分析每个部分具体实现:
LoginModels
import Foundation
enum Login {}
extension Login {
// 登录事件
enum LoginEvent
{
struct Request
{
let account: String?
let password: String?
}
struct Response: Codable
{
let user: User?
let success: Bool
let errorMsg: String?
}
struct ViewModel
{
let success: Bool
let errorMsg: String?
}
}
}
//extension Login {
// // 其他事件
// enum otherEvent {
// struct Request {
//
// }
// struct Response {
//
// }
// struct ViewModel {
//
// }
// }
//}
我们使用Swift的命名空间方式定义LoginModels模块,然后再根据不同事件定义来定义命名空间,在命名空间里面包含该事件对应的Request
、Response
和ViewModel
这三部分内容,而这三部分内容就是VIP之间对应传输的内容。
从LoginEvent
中的三部分可以看出:
- 首先用户登录时需要点击登录按钮,触发LoginViewController的登录事件,需要从用户输入的信息中读取account和password,常见的方式是读取对应的TextField获取信息文本,然后组成
Request
传递给LoginInteractor。 - 而LoginInteractor会进行网络请求后获取数据,数据包含用户信息、请求是否成功、错误信息,而这些数据将组成一个
Response
,同时也有可能会对信息进行数据库存储。 - 接着将
Response
传递给LoginPersenter进行处理,因为用户只关心登录是否成功,假如失败了需要给出提示信息,于是LoginPersenter将Response
处理成视图展示所需要的数据ViewModel
。 - 最后将
ViewModel
传递给LoginViewController用于更新界面。
LoginViewController
import UIKit
/// Presenter output
protocol LoginDisplayLogic: class {
func loginSuccessed(viewModel: Login.LoginEvent.ViewModel)
func loginFailed(viewModel: Login.LoginEvent.ViewModel)
}
class LoginViewController: UIViewController {
/// input
var interactor: LoginBusinessLogic?
var router: (NSObjectProtocol & LoginRoutingLogic & LoginDataPassing)?
@IBOutlet weak var accountTF: UITextField!
@IBOutlet weak var passwordTF: UITextField!
@IBOutlet weak var loginBtn: UIButton!
// MARK: Object lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// MARK: Setup
/// 根据整个VIP架构来设置各个部分之间的关系
private func setup() {
let viewController = self
let interactor = LoginInteractor()
let presenter = LoginPresenter()
let router = LoginRouter()
viewController.interactor = interactor
viewController.router = router
interactor.presenter = presenter
presenter.viewController = viewController
router.viewController = viewController
router.dataStore = interactor
}
// MARK: View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configure()
}
func configure() {
accountTF.text = "haoxian"
passwordTF.text = "123"
}
@IBAction func LoginButtonDidTap(_ sender: UIButton) {
guard let account = accountTF.text, let password = passwordTF.text else {
return print("Fields of account and password may not be empty!")
}
let request = Login.LoginEvent.Request(account: account, password: password)
interactor?.loginAction(request: request)
}
func ToMainView() {
interactor?.fetchRouterDataStroe(with: "8888")
router?.toMainView()
}
}
/// 遵循协议,作为 Presenter 的 output
extension LoginViewController: LoginDisplayLogic {
func loginSuccessed(viewModel: Login.LoginEvent.ViewModel) {
print("login successed.")
ToMainView()
}
func loginFailed(viewModel: Login.LoginEvent.ViewModel) {
if let msg = viewModel.message {
print("login failed, error: \(msg)")
} else {
print("login failed")
}
}
}
我们在初始化LoginViewController时会调用setup()
这个方法,这个方法包含了创建整个VIP架构的每个部分,并且按它们之间的关系进行赋值设置。
在LoginViewController中有两个事件。一个是用户点击登录按钮的事件,将获取的account和password组成Request
传递给LoginInteractor对应的处理方法。另一个事件是跳转到MainView,上面在Router介绍那里也提到跳转到另一个页面可能需要对控制器进行传值初始化,而LoginInteractor是作为LoginRouter的DataStore
来提供数据,于是我们需要完成提供数据前的工作。
LoginViewController里定义了一个interactor
属性(其实就是LoginInteractor)来接收事件生成的Request
并处理。
而LoginViewController遵守了LoginDisplayLogic
协议来接收LoginPersenter传递过来的ViewModel
用于更新视图。
LoginInteractor
import Foundation
/// ViewContoller output
protocol LoginBusinessLogic {
func loginAction(request: Login.LoginEvent.Request)
func fetchRouterDataStroe(with userId: String)
}
/// Router DataStore
protocol LoginDataStore {
var user: User? { get set }
}
class LoginInteractor: LoginDataStore {
var presenter: LoginPresentationLogic?
/// Worker
let networkWorker = LoginNetworkWorker()
/// Worker
let databaseWoeker = LoginDatabaseWorker()
// MARK: - LoginDataStore
var user: User?
}
/// 遵循协议,作为ViewContoller 的 output
extension LoginInteractor: LoginBusinessLogic {
func loginAction(request: Login.LoginEvent.Request) {
guard let account = request.account, let password = request.password else {
let response = Login.LoginEvent.Response(user: nil, success: false, errorMsg: "Fields may not be empty.")
presenter?.presentLoginResult(response)
return
}
// 通过 networkWorker 进行网络部分的操作
networkWorker.fetch(account: account, password: password, complete: { (response) in
if response.success {
// 通过 databaseWoeker 进行数据库部分的操作
self.databaseWoeker.saveUserInfo(response)
}
self.presenter?.presentLoginResult(response)
})
}
func fetchRouterDataStroe(with userId: String) {
// 通过 databaseWoeker 进行数据库部分的操作
user = databaseWoeker.fetchUserInfoFromDatabase(with: userId)
}
}
LoginInteractor里定义了一个presenter
属性(其实就是LoginPersenter)来接收处理后生成的Response
。
而LoginInteractor遵守了LoginBusinessLogic
协议来接收LoginViewController传递过来的Request
用于处理。
同时LoginInteractor也遵守LoginDataStore
协议成为LoginRouter的DataStore
。
在LoginInteractor里定义了两种Worker来负者网络和数据库的操作,在方法内部是通过调用各种Workder来处理事件的。
LoginWorker
import Foundation
typealias reponseHandler = (_ reponse: Login.LoginEvent.Response) -> ()
class LoginNetworkWorker {
func fetch(account: String, password: String, complete: @escaping reponseHandler) {
Network.apiManager.loginFetch(account: account, password: password) { (jsonData) in
let decoder = JSONDecoder()
do {
let reponse = try decoder.decode(Login.LoginEvent.Response.self, from: jsonData)
complete(reponse)
} catch {
print(error)
}
}
}
}
class LoginDatabaseWorker {
func saveUserInfo(_ reponse: Login.LoginEvent.Response) {
if let user = reponse.user {
DataBase.manager.saveUserInfo(user)
}
}
func fetchUserInfoFromDatabase(with userId: String) -> User {
guard let user = DataBase.manager.getUserInfo(with: userId) else { fatalError() }
return user
}
}
LoginWorker是比LoginInteractor更细的业务逻辑处理单位。在这里Worker直接与网络或数据库打交道,并且将数据转成Response
回调给LoginInteractor。
LoginPersenter
import Foundation
/// Interactor output
protocol LoginPresentationLogic {
func presentLoginResult(_ response: Login.LoginEvent.Response)
}
class LoginPresenter {
weak var viewController: LoginDisplayLogic?
}
/// 遵循协议,作为Interactor 的 output
extension LoginPresenter: LoginPresentationLogic {
func presentLoginResult(_ response: Login.LoginEvent.Response) {
let viewModel = Login.LoginEvent.ViewModel(success: response.success, errorMsg: response.errorMsg)
if viewModel.success {
self.viewController?.loginSuccessed(viewModel: viewModel)
} else {
self.viewController?.loginFailed(viewModel: viewModel)
}
}
}
在LoginPersenter里有一个viewController属性(其实就是LoginViewController)来接受处理后生成的ViewModel
。并且这里是使用了weak修饰避免VIP架构间接造成的循环引用。
同时LoginPersenter也遵守了LoginPresentationLogic
协议来接受LoginInterator传递过来的Response
。
LoginRouter
import UIKit
@objc protocol LoginRoutingLogic {
func toMainView()
}
protocol LoginDataPassing {
var dataStore: LoginDataStore? { get }
}
class LoginRouter: NSObject, LoginDataPassing {
weak var viewController: LoginViewController?
var dataStore: LoginDataStore?
}
extension LoginRouter: LoginRoutingLogic {
func toMainView() {
let mainView = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MainViewController") as! MainViewController
/// 这里传值例子只是用于强行演示,实际开发中是不采取这种形式
mainView.user = dataStore?.user
UIApplication.shared.keyWindow?.rootViewController = mainView
}
}
LoginRouter是一个NSObject
对象,对于LoginViewController能跳转的页面的方法都写在LoginRoutingLogic
协议里面,而这个协议被标记了@objc
运行时标识,因此里面的方法能使用运行时的功能,例如performSeletor
这些OC中常用的运行时方法。同时LoginRouter遵守LoginDataPassing
协议,拥有dataStore
属性(其实就是LoginInteractor)用于提供跳转页面时需要传递的数据。
总结
对于一些界面功能复杂的模块,VIP结构能够按功能细分成许多部分。在测试维护时,对于出错的部分可以更快的定位到错误代码。对于中途接手项目的人可以更好的理解这个模块的组成。在一些大型项目中,VIP是一个能够信任的架构,因为它能够很好地工作并且带来比MVC架构更大的优势。不过对于一些功能比较简单的模块,使用其他更加简单的架构还是更加有效率的。可能你会说每当有一个模块使用这个架构都需要创建那么多文件还要设置每个部分的关系显得特别麻烦,你可以从官网进行订阅然后下载模板或者在这里下载模板。
参考文献:Clean Swift
本文Demo:Demo