iOS体验VIP架构

作者: 砖头很烫手 | 来源:发表于2018-04-21 00:08 被阅读27次

    前言

    对于一个设计比较简单的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

    相关文章

      网友评论

      • Molue_James:稍微看了一下你的文章, 觉得写的不错, 把我想做的很多都写了, 给点建议吧, 就是private func setup() 这个方法里面的代码可以放进一个构造器中, 这样看上去更好点, 有兴趣的话, 可以去看一下Uber的RIBs的架构.
        砖头很烫手:学习学习:smile:

      本文标题:iOS体验VIP架构

      本文链接:https://www.haomeiwen.com/subject/dydakftx.html