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

负责视图的展示

它作为Presenteroutput,从Presenter获取的ViewModel来对视图进行数据的赋值,ViewController本身不会对数据进行任何的处理,只会从ViewModel中获取,ViewModel给什么数据,它就展示什么数据。
同时它也作为Interactorinput,当用户与界面进行交互时,它就将这个事件和处理这个事件所需要的参数作为一个Request提供给Interactor,让Interactor来处理这个交互事件。

Interactor

处理用户与界面交互时产生的事件

它作为ViewControlleroutput,当ViewController与用户触发了交互事件,Interactor就会从ViewController提供过来的Request中获取必要的参数来进行处理(比如网络请求、数据库查询),其实它本身是一个事件处理的管理者,它底下还有很多为它服务的工人(Worker),例如负者网络请求的工人、数据库查询的工人,这些工人才是真正处理事件的基本单位(后面会有更详细的图片来好好介绍)。
同时它也作为Presenterinput,当工人获取了数据之后,将结果封装成一个Response然后提供给Presenter来处理。

Presenter

将数据处理成视图所要展示的内容

它作为Interactoroutput,将Interactor提供的Response进行处理,有时我们只需要Response其中的一些数据,或者将数据排序什么的一些处理,都在这里进行,处理完后将数据封装成ViewModel
同时它也作为ViewControllerinput,为ViewController展示视图时提供所必须的并且已经处理好的数据。


介绍完这个架构最主要的VIP部分,接下来我们来看看这个架构的全貌
架构全貌

比起上面的图,这张图片多了WorkerModelsRouter。(图片做的很简陋,大家别吐槽...)
因此一个完整的VIP架构包含了:

  • ViewContoller
  • Interactor
  • Presenter
  • Worker
  • Models
  • Router

Worker

上面在介绍Interactor的时候也提到过了,它们是在Interactor里真正处理事件的基本单位,每个Worker有它们单一的职责。需要进行网络请求的任务,就调用负责网路请求的Worker,需要进行数据库更新或者获取的任务,就调用负责数据库处理的Worker,我们可以按事件类型来定义许多不同的WorkerWorker处理完后将处理回调给Interactor

Models

当在ViewController中与用户产生交互事件时,从ViewControllerInteractor再到Presentrt最后回到ViewController都会产生一系列的RequestResponseViewModel。而对于每个特定的事件,都有特定的RequestResponseViewModel,于是在Models中,我们会利用Swift独特命名空间的方式将每个事件定义一个命名空间,而这个事件的命名空间里包含了RequestResponseViewModel这三个Model

  • Request:网络请求或数据库查询所需要的一些参数。
  • Response:网络请求或数据库查询返回的一些数据。
  • ViewModel:将Response处理成视图需要展示的数据。

Router

路由器,顾名思义就是用来进行页面跳转的。在Router里面,我们需要根据根据情况是否通过segue跳转,或者通过StoryboardXIB来创建,或则直接通过代码创建,并且对新控制器进行初始化赋值,最后进行跳转。对于新控制器的初始化赋值我们是通过Router里面的DataStore属性来进行获取的,而作为这RouterDataStore就是Interactor,因为页面跳转也属于用户交互行为,是一个跳转事件,在跳转前需要做跳转前的处理,处理完后,Router在从Interactor获取数据来将控制器初始化。


Demo体验

对于VIP架构每个部分已经介绍完了,让我们通过万能的登录Demo实际感受一下这个架构。

先看看看Demo的目录:
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模块,然后再根据不同事件定义来定义命名空间,在命名空间里面包含该事件对应的RequestResponseViewModel这三部分内容,而这三部分内容就是VIP之间对应传输的内容。
LoginEvent中的三部分可以看出:

  1. 首先用户登录时需要点击登录按钮,触发LoginViewController的登录事件,需要从用户输入的信息中读取account和password,常见的方式是读取对应的TextField获取信息文本,然后组成Request传递给LoginInteractor
  2. LoginInteractor会进行网络请求后获取数据,数据包含用户信息、请求是否成功、错误信息,而这些数据将组成一个Response,同时也有可能会对信息进行数据库存储。
  3. 接着将Response传递给LoginPersenter进行处理,因为用户只关心登录是否成功,假如失败了需要给出提示信息,于是LoginPersenterResponse处理成视图展示所需要的数据ViewModel
  4. 最后将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是作为LoginRouterDataStore来提供数据,于是我们需要完成提供数据前的工作。
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协议成为LoginRouterDataStore
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

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

推荐阅读更多精彩内容