探究在iOS開發(fā)中實現(xiàn)VIPER架構(gòu)

前言

在軟件開發(fā)中桨踪,架構(gòu)是至關重要的一部分老翘,就好比蓋房子需要基本的鋼筋石樁等骨架,常聽到的架構(gòu)有MVC锻离、MVP铺峭、MVVMVIPER等纳账,其中逛薇,MVC是我們最常用的軟件架構(gòu)模式,而蘋果的整個API框架都是使用MVC作為架構(gòu)的疏虫,所以我們會看到一些iOS的API中有這些類:UIXXXViewControllerUIXXXView,而現(xiàn)在比較興起的架構(gòu)當屬MVPMVVM卧秘,我個人覺得這它們是非常相似的呢袱,但在之前我使用第三方函數(shù)式、響應式框架RxSwiftReactiveCocoa去實現(xiàn)MVPMVVM架構(gòu)時翅敌,我自認為羞福,MVP中的Presenter專注于事件、數(shù)據(jù)的轉(zhuǎn)換蚯涮,成為View層及Model層的一條流通管道治专,而MVVM中的ViewModel更像是一個裝有視圖顯示數(shù)據(jù)的,并帶有一些顯示邏輯處理的分層遭顶,然后我們可以將ViewModel中的顯示數(shù)據(jù)與View中的視圖進行響應式綁定(個人觀點张峰,若有誤,望各位糾正)棒旗。在現(xiàn)在的開發(fā)中喘批,我也是使用MVPMVVM架構(gòu)。而VIPER架構(gòu)铣揉,一開始我是只聽過其名饶深,并未深入了解,也并未實戰(zhàn)使用逛拱,直到某個契機我看到大神@羅琦aidenluo的iOS架構(gòu)講解視頻敌厘,了解到了VIPER架構(gòu),受益匪淺朽合,這篇文章是我對VIPER學習以及實踐的總結(jié)额湘,主要簡單介紹VIPER架構(gòu)以及其怎樣使用Swift3.0語言在iOS平臺上實現(xiàn)。

文章所對應的代碼我已經(jīng)放到了我的Github上TanVIPER旁舰,歡迎Click入~

什么是 VIPER

傳統(tǒng)的MVC架構(gòu)中锋华,我們都知道,其Controller(控制器)層接納了太多的任務箭窜,當開發(fā)不斷進行毯焕,其內(nèi)部的業(yè)務邏輯逐漸積累,最后則會變得臃腫不堪磺樱,不便于后期的調(diào)試測試以及多人協(xié)助纳猫,所以,我們需要尋找減輕Controller層負擔的方法竹捉,而VIPER架構(gòu)其實是將Controller再細分成三層芜辕,分別是ViewInteractor块差、Presenter侵续,已達到減輕Controller層負擔的作用倔丈。

VIPER中每個字母的意思是如下:

  • V: View 視圖:在這里并不是指傳統(tǒng)的UIView或其子類,事實上它就是UIViewController状蜗,在前面所說到需五,VIPER架構(gòu)主要是將MVC架構(gòu)中的Controller進行更加細致的劃分,而View(視圖)層則是主要負責一些視圖的顯示轧坎、布局宏邮,用戶事件的接受以及轉(zhuǎn)發(fā),基本的顯示邏輯處理等等工作缸血。
  • I: Interactor 交互器:其為VIPER的中心樞紐蜜氨,主要負責交互的工作,例如數(shù)據(jù)的請求(網(wǎng)絡請求捎泻、本地持久化層請求)飒炎、某些業(yè)務邏輯的處理,在這里我們得到的數(shù)據(jù)是原始數(shù)據(jù)族扰,需要經(jīng)過解析處理轉(zhuǎn)換成能夠直接應用于視圖的視圖模型數(shù)據(jù)厌丑,所以我們需要用到了下一層Presenter(展示器)
  • P: Presenter 展示器:當我們在上一層Interactor(交互器)中獲得原始數(shù)據(jù)后渔呵,我們需要將數(shù)據(jù)進行解析處理怒竿,比如我們在交互器中進行了網(wǎng)絡請求,得到了json數(shù)據(jù)扩氢,若要將json中所包含的內(nèi)容顯示出來耕驰,我們則需要將json數(shù)據(jù)進行解析,展示器就是專注于數(shù)據(jù)的解析轉(zhuǎn)換录豺,將原始的數(shù)據(jù)轉(zhuǎn)換成最終能夠直接顯示在試圖上的視圖模型數(shù)據(jù)朦肘。此外,展示器中還帶有路由器Router双饥,可以進行路由的操作媒抠。
  • E: Entity 實體模型對象
  • R: Router 路由器: 負責視圖的跳轉(zhuǎn),因為使用VIPER架構(gòu)需要進行各層之間的相互綁定咏花,所以視圖的跳轉(zhuǎn)不能簡單地使用原始的方法趴生。

下面是一張VIPER的簡單邏輯圖:


圖中,箭頭代表著數(shù)據(jù)流的傳遞昏翰,我們可以看到苍匆,在VIPER架構(gòu)中,數(shù)據(jù)的流向總是單向流動棚菊,在View浸踩、InteractorPresenter三層中形成了一個流動閉環(huán)统求,而在其他的某些架構(gòu)中检碗,如MVC据块、MVPMVVM后裸,它們的數(shù)據(jù)在中間層會有著雙向的流動瑰钮,VIPER較它們而言冒滩,其更加約束了整個軟件的架構(gòu)微驶,每一層功能特定,數(shù)據(jù)的流向單一开睡,使得軟件在開發(fā)中對原架構(gòu)的高度切合因苹。

如何配置 VIPER

在對VIPER架構(gòu)的實現(xiàn)中,我是基于@羅琦aidenluoVIP架構(gòu)思想篇恒,稍作添加改動扶檐。使用的語言是Swift 3.0

協(xié)議

我們先指定好一套協(xié)議胁艰,用于規(guī)范好VIPER各層間的綁定與聯(lián)系款筑。

//  MARK: - Protocol
protocol ViewToInteratorPipline {
    func refresh(request: Request)
}

protocol InteratorToPresenterPipline {
    func present(response: Response)
}

protocol PresenterToViewPipline {
    func display(viewModel: ViewModel)
}

protocol Request { }

protocol Response { }

protocol ViewModel { }

如上,有三個管道協(xié)議腾么,用于連通View奈梳、InteractorPresenter三層解虱;在View通向Interactor管道中攘须,通過方法refresh(request:)來讓View請求Interactor去進行刷新;在Interactor通向Presenter管道中殴泰,通過方法present(response:)來讓Interactor將原始數(shù)據(jù)傳遞給Presenter讓其進行數(shù)據(jù)的解析處理于宙;在Presenter通向View管道中,通過display(viewModel:)方法來讓Presenter將視圖模型傳遞給View然后讓其顯示悍汛。三層環(huán)環(huán)相扣捞魁。

抽象基類

在之前曾想過使用Swift的面向協(xié)議編程來對各層進行實現(xiàn),但是考慮到一些動態(tài)創(chuàng)建以及各層的綁定問題离咐,所以最后使用的是抽象基類方法谱俭。

//  MARK: - Abstract Class
class View: ViewController, PresenterToViewPipline {
    
    final let interator: Interactor
    
    required init(interator: Interactor) {
        self.interator = interator
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func display(viewModel: ViewModel) {
        fatalError("display(viewModel:) is an abstract function")
    }
    
    func show(route: Router, userInfo: Any?) {
        fatalError("show(route:userInfo:) is an abstract function")
    }
}

class Interactor: ViewToInteratorPipline {
    
    final let presenter: Presenter
    
    required init(presenter: Presenter) {
        self.presenter = presenter
    }
    
    func refresh(request: Request) {
        fatalError("refresh(request:) is an abstract function")
    }
}

class Presenter: InteratorToPresenterPipline {
    
    private final weak var _view: View? {  //  !! Weak !!
        didSet {
            self._router = Router(presenter: self)
        }
    }
    
    private final var _router: Router?
    
    final var view: View? {
        set {
            assert(self._view == nil, "view has already set!")
            self._view = newValue
        }
        
        get {
            return self._view
        }
    }
    
    final var router: Router? {
        get {
            return self._router
        }
    }
    
    required init() { }
    
    func present(response: Response) {
        fatalError("response(Response:) is an abstract function")
    }
}

如上代碼所示,定義了三個抽象基類健霹,分別代表了View旺上、InteractorPresenter三層糖埋,它們各自實現(xiàn)了管道協(xié)議宣吱,每一個抽象基類中都持有其下一層的基類,在構(gòu)造方法中進行初始化瞳别。如View類中持有了Interactor類的屬性征候,作用是進行層與層之間的數(shù)據(jù)傳輸杭攻。
這里細講一下:

  • View是直接繼承于ViewController的,所以在VIPER中疤坝,我們將View指代了ViewController兆解,并且,View除了實現(xiàn)管道協(xié)議外跑揉,其內(nèi)部還有一個show(router:userInfo:)的抽象方法锅睛,此方法可用于路由跳轉(zhuǎn)時數(shù)據(jù)的傳輸,將一些數(shù)據(jù)在跳轉(zhuǎn)前傳輸?shù)侥繕颂D(zhuǎn)視圖中历谍。
  • Presenter中的Viewweak弱引用類型现拒,因為在ViewInteractor望侈、Presenter三層綁定時有引用環(huán)形成印蔬,如果不將引用環(huán)中的某個引用設為弱引用,則會出現(xiàn)循環(huán)引用現(xiàn)象脱衙。此外侥猬,Presenter中還具有路由器Router,我們在Presenter中可以利用路由器進行頁面的跳轉(zhuǎn)捐韩。

綁定器與聯(lián)合體

我們在使用VIPER時退唠,需要將各層進行綁定,比如OneView的交互器要綁定OneInteractor奥帘,而OneInteractor的展示器要綁定OnePresenter铜邮,因為綁定的操作頻繁,所以我這里將層之間的綁定操作封裝成了綁定器Binder寨蹋。聯(lián)合體就是將要綁定在一起的View松蒜、InteractorPresenter封裝成模型已旧。

//  MARK: - Unity
struct Unity {
    let viewType: View.Type
    let interatorType: Interactor.Type
    let presenterType: Presenter.Type
}

extension Unity: ExpressibleByArrayLiteral {
    
    typealias Element = AnyClass
    
    init(arrayLiteral elements: Unity.Element...) {
        assert(elements.count == 3)
        guard let viewType = elements[0] as? View.Type else { assert(false) }
        guard let interactorType = elements[1] as? Interactor.Type else { assert(false) }
        guard let presenterType = elements[2] as? Presenter.Type else { assert(false) }
        self.viewType = viewType
        self.interatorType = interactorType
        self.presenterType = presenterType
    }
}

//  MARK: - Binder
class Binder {
    
    static var unitySet: [String: Unity] = [:]
    
    static func addUnity(_ unity: Unity, identifier: String) {
        self.unitySet[identifier] = unity
    }
    
    static func obtainView(identifier: String) -> View? {
        guard let unity = self.unitySet[identifier] else { return nil }
        
        //  Bind
        let presenter = unity.presenterType.init()
        let interator = unity.interatorType.init(presenter: presenter)
        let view = unity.viewType.init(interator: interator)
        presenter.view = view
        return view
    }
    
}
  • Unity 聯(lián)合體實現(xiàn)了字面量表達式的協(xié)議秸苗,我們能直接通過列表來構(gòu)建聯(lián)合體,而在聯(lián)合體中儲存的是三個分層的類型运褪,用于綁定器的分層動態(tài)生成與綁定惊楼。
  • Binder 綁定器職責是將其里面儲存的聯(lián)合體中的三個分層進行綁定, 我們通過obtainView(identifier:)方法秸讹,傳入標識符對View進行索取檀咙,在此方法返回前,就自動幫我們進行三層的綁定璃诀。在對View進行索取前弧可,必須先進行聯(lián)合體的添加配置,使用的是addUnity(_, identifier:)方法劣欢,一般我們可以在AppDelegateapplication(_, didFinishLaunchingWithOptions:)方法中進行綁定器的初始化配置棕诵。

路由器

路由器主要是負責視圖的跳轉(zhuǎn)裁良,它位于Presnter層,以下是它的代碼:

//  MARK: - Router
enum RouteType {
    case root(identifier: String)
    case push(identifier: String)
    case modal(identifier: String)
    case back
}

extension RouteType {
    var identifier: String? {
        switch self {
        case let .root(identifier):
            return identifier
        case let .push(identifier):
            return identifier
        case let .modal(identifier):
            return identifier
        default:
            return nil
        }
    }
    
    var view: View? {
        guard let identifier = self.identifier else { return nil}
        return Binder.obtainView(identifier: identifier)
    }
}

class Router {
    
    let presenter: Presenter?
    
    required init(presenter: Presenter? = nil) {
        self.presenter = presenter
    }
    
    func route(type: RouteType, userInfo: Any?) {
        let view = type.view
        view?.show(router: self, userInfo: userInfo)
        switch type {
        case .root:
            UIApplication.shared.keyWindow?.rootViewController = view
        case .push:
            if let view = view { self.presenter?.view?.navigationController?.pushViewController(view, animated: true) }
        case .modal:
            if let view = view { self.presenter?.view?.present(view, animated: true, completion: nil) }
        case .back:
            guard let view = presenter?.view else { return }
            if view.presentationController != nil {
                view.dismiss(animated: true, completion: nil)
            } else {
                _ = view.navigationController?.popViewController(animated: true)
            }
        }
    }
}

我定義的這個路由器比較簡單校套,有四種跳轉(zhuǎn)的方式:

  1. 模態(tài)跳轉(zhuǎn)
  2. 導航跳轉(zhuǎn)
  3. 根視圖切換
  4. 返回
    其中价脾,根視圖切換是針對應用程序主窗口KeyWindow的根視圖進行切換,一般在應用程序啟動時應用笛匙。

這里侨把,我們進行跳轉(zhuǎn)不像是傳統(tǒng)的那樣傳入ViewController實例,而是直接傳入聯(lián)合體的標識符膳算,路由器會利用此標識符經(jīng)過綁定器的動態(tài)生成及綁定座硕,獲取到要跳轉(zhuǎn)的視圖弛作,從而進行跳轉(zhuǎn)涕蜂。
在跳轉(zhuǎn)時,我們可以將一些附帶數(shù)據(jù)傳入userInfo參數(shù)中映琳,這些數(shù)據(jù)能在跳轉(zhuǎn)前于目標跳轉(zhuǎn)視圖的show(router:userInfo:)方法中獲取到机隙。


到此,VIPER架構(gòu)的基本配置就已經(jīng)搭好了萨西。

使用 VIPER

下面我們通過VIPER架構(gòu)來做一個實例有鹿,主要包含兩個需求,一個是用戶的登錄谎脯,另一個是視圖的跳轉(zhuǎn)葱跋。
上GIF圖~


如圖所示,主頁面有兩個按鈕源梭,一個是用于將視圖跳轉(zhuǎn)到另一個頁面娱俺,二哥則是將輸入的用戶名及密碼進行驗證登錄。

下面就開工吧~

服務器端構(gòu)建

服務器端這里我寫的比較簡單废麻,只是進行一些死數(shù)據(jù)的判斷以及json輸出荠卷,使用的是PHP語言:

<?php
//  TanVIPER Server

$userName = isset($_POST['user_name']) ? $_POST['user_name'] : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';

$out = check($userName, $password);
echo json_encode($out, JSON_UNESCAPED_UNICODE);

//  Code: 200  --> Success , 300  --> Faild
//  Function
function check($userName, $password) {
    if ($userName == 'tangent') {
        if ($password == '123456') {
            $userInfo = array('name' => 'tangent', 'gender' => 1, 'token' => '11233', 'age' => 20);
            return array('code' => 200, 'message' => '登錄成功', 'user_info' => $userInfo);
        } else {
            return array('code' => 300, 'message' => '密碼錯誤');
        }
    } else {
        return array('code' => 300, 'message' => '不存在此用戶');
    }
}
?>

接下來,就是手機iOS端的搭構(gòu)

依賴

在此實例中涉及了網(wǎng)絡請求烛愧、json數(shù)據(jù)解析油宜、自動布局等等需求,所以我們利用CocoaPods引入一些第三方依賴庫怜姿。

  • Moya 用于網(wǎng)絡請求
  • SnapKit 用于自動布局
  • Argo慎冤、Curry 用于JSON數(shù)據(jù)轉(zhuǎn)模型

實體 Entity

這個項目有兩個聯(lián)合體,我分別起名叫OneTwo

//  MARK: - VIPERs
enum VIPERs: String {
    case one
    case two
}

extension VIPERs {
    var identifier: String {
        return self.rawValue
    }
}

在前面說到沧卢,綁定器以及路由都是通過聯(lián)合體的標識符來唯一標識的蚁堤,所以這里我讓枚舉的原始值類型為字符串,并在擴展中添加了獲取標識符的方法搏恤。


針對不同的聯(lián)合體违寿,Request湃交、ResponseViewModel有所不同藤巢,所以這里我們定義兩個聯(lián)合體的各種實體模型:

//  One
enum OneRequest: Request {
    case jump
    case login(userName: String, password: String)
}

enum OneResponse: Response {
    case jumpResponse(viper: VIPERs)
    case loginResponse(json: Any?)
}

struct OneViewModel: ViewModel {
    
    let alertMessage: String
    
    init(alertMessage: String) {
        self.alertMessage = alertMessage
    }
}

//  Two
enum TwoRequest: Request {
    case back
}

enum TwoResponse: Response {
    case back
}

當我們啟動應用時搞莺,我們需要對Binder(綁定器)進行初始化,將應用的所以聯(lián)合體進行添加配置掂咒,這里我就封裝了一個結(jié)構(gòu)體才沧,專門用于綁定器的初始化:

//  MARK: - BinderHelper
struct BinderHelper {
    static func initBinder() {
        Binder.addUnity([OneView.self, OneInteractor.self, OnePresenter.self], identifier: VIPERs.one.identifier)
        Binder.addUnity([TwoView.self, TwoInteractor.self, TwoPresenter.self], identifier: VIPERs.two.identifier)
    }
}

我們在應用剛啟動的時候就可以調(diào)用里面的初始化方法。


我將從網(wǎng)絡獲取到的響應數(shù)據(jù)以及其中的用戶數(shù)據(jù)封裝成一個模型實體:

//  MARK: - User
enum UserGender: String {
    case male = "男"
    case female = "女"
}

struct User {
    let name: String
    let age: Int
    let gender: UserGender
    let token: String
}

extension User: CustomStringConvertible {
    var description: String {
        return "姓名: \(self.name), 年齡: \(self.age), 性別: \(self.gender.rawValue), 令牌: \(self.token)"
    }
}

extension User: Decodable {
    
    static func decode(_ json: JSON) -> Decoded<User> {
        
        let genderMapper: (Int) -> UserGender = { genderType in
            if genderType == 1 {
                return .male
            } else {
                return .female
            }
        }
        
        return curry(self.init)
            <^> json <| "name"
            <*> json <| "age"
            <*> (genderMapper <^> json <| "gender")
            <*> json <| "token"
    }
}

//  MARK: - Network Response
enum NetworkResponse {
    case faild(message: String)
    case success(user: User)
}

extension NetworkResponse: Decodable {
    
    init(code: Int, message: String, userInfo: User?) {
        if let user = userInfo, code == 200 {
            self = .success(user: user)
        } else {
            self = .faild(message: message)
        }
    }
    
    static func decode(_ json: JSON) -> Decoded<NetworkResponse> {
        return curry(self.init)
            <^> json <| "code"
            <*> json <| "message"
            <*> json <|? "user_info"
    }
}

其中绍刮,實體的Decodable擴展是Argo框架中用于json的數(shù)據(jù)轉(zhuǎn)模型的實現(xiàn)温圆。


由于我們使用了網(wǎng)絡請求框架Moya,它需要我們提供一個請求的目標實體:

//  MARK: - Network Request
enum NetworkRequest {
    case login(userName: String, password: String)
}

extension NetworkRequest: TargetType {
    
    var baseURL: URL {
        return URL(string: "http://127.0.0.1")!
    }
    
    var path: String {
        switch self {
        case .login:
            return "/projects/tanviper.php"
        }
    }
    
    var method: Moya.Method {
        return .post
    }
    
    var parameters: [String: Any]? {
        switch self {
        case let .login(userName, password):
            return ["user_name": userName, "password": password]
        }
    }
    
    var sampleData: Data {
        return "{\"code\": \"300\", \"message\": \"不存在此用戶\"}".data(using: .utf8)!
    }
    
    var task: Task {
        return .request
    }
}

這個請求實體只有一項登錄功能孩革,在這里岁歉,我連接的是本地的服務器。

One 聯(lián)合體

接下來就開始構(gòu)建聯(lián)合體了膝蜈,先看回上面所說到的用于初始化綁定器的實體的綁定器初始化方法:

        Binder.addUnity([OneView.self, OneInteractor.self, OnePresenter.self], identifier: VIPERs.one.identifier)
        Binder.addUnity([TwoView.self, TwoInteractor.self, TwoPresenter.self], identifier: VIPERs.two.identifier)

我們可以看到锅移,對于One聯(lián)合體來說,它的組成為OneView饱搏、OneInteractor非剃、OnePresenter,對于Two聯(lián)合體來說是TwoView推沸、TwoInteractor备绽、TwoPresenter,所以我們需要創(chuàng)建這兩個聯(lián)合體的每個組成部分鬓催。

對于One聯(lián)合體:

View

import UIKit
import SnapKit

class OneView: View {

    //  MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white
        
        self.buttonListener = OneViewButtonListener(jump: { 
            self.interator.refresh(request: OneRequest.jump)
        }, login: {
            self.interator.refresh(request: OneRequest.login(userName: self.userNameInput.text!, password: self.passwordInput.text!))
            self.loginButton.isEnabled = false
        })
        
        self.view.addSubview(self.jumpButton)
        self.view.addSubview(self.loginButton)
        self.view.addSubview(self.userNameInput)
        self.view.addSubview(self.passwordInput)
        
        self.layoutViews()
    }

    //  Override
    override func display(viewModel: ViewModel) {
        self.loginButton.isEnabled = true
        let alertMessage = (viewModel as! OneViewModel).alertMessage
        self.alertController.message = alertMessage
        self.present(alertController, animated: true, completion: nil)
    }
    
    override func show(router: Router, userInfo: Any?) {
        
    }
    
    //  MARK: - Pirvate Function
    private func layoutViews() {
        let viewHeight: CGFloat = 45
        let viewMargin: CGFloat = 30
        
        self.jumpButton.snp.makeConstraints { [unowned self] maker in
            maker.height.equalTo(viewHeight)
            maker.left.right.equalTo(self.view).inset(UIEdgeInsets(top: 0, left: viewMargin * 0.5, bottom: 0, right: viewMargin))
            maker.bottom.equalTo(self.view.snp.centerY).offset(-viewMargin)
        }
        
        self.loginButton.snp.makeConstraints { [unowned self] maker in
            maker.height.left.right.equalTo(self.jumpButton)
            maker.top.equalTo(self.view.snp.centerY).offset(viewMargin * 0.5)
        }
        
        self.userNameInput.snp.makeConstraints { [unowned self] maker in
            maker.height.left.right.equalTo(self.jumpButton)
        }
        
        self.passwordInput.snp.makeConstraints { [unowned self] maker in
            maker.height.left.right.equalTo(self.jumpButton)
            maker.top.equalTo(self.userNameInput.snp.bottom).offset(viewMargin)
            maker.bottom.equalTo(self.jumpButton.snp.top).offset(-viewMargin)
        }
    }
    
    private func initButton(_ button: UIButton, title: String, onClick: Selector) -> UIButton {
        button.setTitle(title, for: .normal)
        button.backgroundColor = UIColor.orange
        button.layer.masksToBounds = true
        button.layer.cornerRadius = 8
        button.setTitleColor(UIColor.white, for: .normal)
        button.setTitleColor(UIColor.gray, for: .highlighted)
        button.addTarget(self.buttonListener, action: onClick, for: .touchUpInside)
        return button
    }
    
    private func initTextField(_ textField: UITextField, placeHolder: String) -> UITextField {
        textField.backgroundColor = UIColor.green
        textField.textColor = UIColor.darkGray
        textField.layer.masksToBounds = true
        textField.layer.cornerRadius = 8
        textField.placeholder = placeHolder
        return textField
    }
    
    //  MARK: - Lazy
    private lazy var jumpButton: UIButton = {
        return self.initButton($0, title: "跳轉(zhuǎn)", onClick: #selector(OneViewButtonListener.onJumpButtonClick))
    }(UIButton())
    
    private lazy var loginButton: UIButton = {
        return self.initButton($0, title: "登錄", onClick: #selector(OneViewButtonListener.onLoginButtonClick))
    }(UIButton())
    
    private lazy var userNameInput: UITextField = {
        return self.initTextField($0, placeHolder: "用戶名")
    }(UITextField())
    
    private lazy var passwordInput: UITextField = {
        return self.initTextField($0, placeHolder: "密碼")
    }(UITextField())
    
    private lazy var alertController: UIAlertController = {
        let action = UIAlertAction(title: "確認", style: .default, handler: nil)
        $0.addAction(action)
        return $0
    }(UIAlertController(title: "登錄提示", message: nil, preferredStyle: .alert))
    
    //  MARK: - Button Listener
    private var buttonListener: OneViewButtonListener?
    
    //  MARK: - Event
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
}

fileprivate class OneViewButtonListener {
    
    let jumpButtonClickCallback: () -> ()
    let loginButtonClickCallback: () -> ()
    
    init(jump: @escaping () -> (), login: @escaping () -> ()) {
        self.jumpButtonClickCallback = jump
        self.loginButtonClickCallback = login
    }
    
    @objc func onJumpButtonClick() {
        self.jumpButtonClickCallback()
    }
    
    @objc func onLoginButtonClick() {
        self.loginButtonClickCallback()
    }
}

View中肺素,進行的是視圖的顯示、布局以及用戶事件的轉(zhuǎn)發(fā)深浮,可以看到压怠,當兩個按鈕被用戶點擊時,Interactorrefresh(request:)方法會被調(diào)用飞苇,事件及數(shù)據(jù)轉(zhuǎn)發(fā)到了Interactor中菌瘫。

Interactor

import UIKit
import Moya

class OneInteractor: Interactor {
    
    let provider: MoyaProvider<NetworkRequest> = MoyaProvider<NetworkRequest>()
    
    override func refresh(request: Request) {
        let request = request as! OneRequest
        switch request {
        case .jump:
            self.presenter.present(response: OneResponse.jumpResponse(viper: .two))
        case let .login(userName, password):
            self.provider.request(.login(userName: userName, password: password), completion: { result in
                var json: Any? = nil
                switch result {
                case .failure: ()
                case let .success(response):
                    json = try? response.mapJSON()
                }
                self.presenter.present(response: OneResponse.loginResponse(json: json))
            })
        }
    }
}

在這里,我們接收到上一層View傳來的請求數(shù)據(jù)布卡,根據(jù)這些請求雨让,我們進一步處理:

  • 當接收到跳轉(zhuǎn)請求時,通知展示器進行路由跳轉(zhuǎn)
  • 當接收到登錄請求是忿等,向網(wǎng)絡發(fā)送請求栖忠,并將得到的請求結(jié)果json數(shù)據(jù)傳遞到展示器要求其進行解析。

Presenter

import UIKit
import Argo

class OnePresenter: Presenter {
    override func present(response: Response) {
        let response = response as! OneResponse
        switch response {
        case let .jumpResponse(viper):
            self.router?.route(type: .modal(identifier: viper.identifier), userInfo: "From One To Two | One --> Two")
        case let .loginResponse(json):
            var alertMessage = ""
            if let json = json {
                let networkResponse: NetworkResponse = decode(json)!
                switch networkResponse {
                case let .faild(message):
                    alertMessage = "登錄失敗,\(message)"
                case let .success(user):
                    alertMessage = "登錄成功,\(user)"
                }
            } else {
                alertMessage = "網(wǎng)絡請求或數(shù)據(jù)解析錯誤"
            }
            self.view?.display(viewModel: OneViewModel(alertMessage: alertMessage))
        }
    }
}

展示器可通過自身的路由器屬性進行頁面的跳轉(zhuǎn),在跳轉(zhuǎn)時能夠向目標視圖傳遞數(shù)據(jù)庵寞,就想這里我們向目標試圖傳遞了一串字符串狸相。當接收到上一層Interactor的原始數(shù)據(jù)后,展示器進行解析處理捐川,然后最后輸出能夠直接應用于視圖顯示的視圖模型ViewModel脓鹃,通知視圖層去顯示。

Two 聯(lián)合體

Two 聯(lián)合體相對較簡單古沥,這里我只列出了代碼瘸右,不做解釋。

//  MARK: - View
class TwoView: View {
    
    var showMessage: String?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.orange
        self.view.addSubview(self.showView)
        self.showView.snp.makeConstraints { [unowned self] maker in
            maker.center.equalTo(self.view)
        }
        self.showView.text = self.showMessage
    }
    
    override func show(router: Router, userInfo: Any?) {
        self.showMessage = userInfo as? String
    }
    
    //  MARK: - Lazy
    private lazy var showView: UILabel = {
        $0.textColor = UIColor.white
        $0.font = UIFont.systemFont(ofSize: 23)
        $0.textAlignment = .center
        return $0
    }(UILabel())
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.interator.refresh(request: TwoRequest.back)
    }
    
    //  Status Bar Style
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }

}

//  MARK: - Interactor
class TwoInteractor: Interactor {
    override func refresh(request: Request) {
        self.presenter.present(response: TwoResponse.back)
    }
}

//  MARK: - Presenter
class TwoPresenter: Presenter {
    override func present(response: Response) {
        switch response as! TwoResponse {
        case .back:
            self.router?.route(type: .back, userInfo: nil)
        }
    }
}

AppDelegate

最后岩齿,我們需要在AppDelegate中進行應用程序初始化配置:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        //  Init Window
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.backgroundColor = UIColor.white
        window.makeKeyAndVisible()
        self.window = window
        //  Init Binder
        BinderHelper.initBinder()
        //  Router
        Router().route(type: .root(identifier: VIPERs.one.identifier), userInfo: nil)
        return true
    }

到此為止太颤,整個基于VIPER架構(gòu)的小Demo就完成了。

總結(jié) & 鏈接

本文架構(gòu)設計靈感源于@羅琦aidenluoVIP架構(gòu)設計思想盹沈,在這里我也感謝大神的指點龄章,讓我對VIPER架構(gòu)有著更深層的了解。

本人為iOS開發(fā)菜鳥一只襟诸,若文章中某些話語不嚴謹或出現(xiàn)技術性錯誤瓦堵,還請各位提點意見,也歡迎各位在評論區(qū)進行討論歌亲,在這里也祝大家冬日愉快~

文章中實例的Github鏈接:TanVIPER

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市澜驮,隨后出現(xiàn)的幾起案子陷揪,更是在濱河造成了極大的恐慌,老刑警劉巖杂穷,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悍缠,死亡現(xiàn)場離奇詭異,居然都是意外死亡耐量,警方通過查閱死者的電腦和手機飞蚓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來廊蜒,“玉大人趴拧,你說我怎么就攤上這事∩蕉#” “怎么了著榴?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長屁倔。 經(jīng)常有香客問我脑又,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任问麸,我火速辦了婚禮往衷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘严卖。我一直安慰自己炼绘,他們只是感情好,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布妄田。 她就那樣靜靜地躺著俺亮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疟呐。 梳的紋絲不亂的頭發(fā)上脚曾,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機與錄音启具,去河邊找鬼本讥。 笑死,一個胖子當著我的面吹牛鲁冯,可吹牛的內(nèi)容都是我干的拷沸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼薯演,長吁一口氣:“原來是場噩夢啊……” “哼撞芍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起跨扮,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤序无,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后衡创,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帝嗡,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年璃氢,在試婚紗的時候發(fā)現(xiàn)自己被綠了哟玷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡一也,死狀恐怖巢寡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塘秦,我是刑警寧澤讼渊,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站尊剔,受9級特大地震影響爪幻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一挨稿、第九天 我趴在偏房一處隱蔽的房頂上張望仇轻。 院中可真熱鬧,春花似錦奶甘、人聲如沸篷店。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疲陕。三九已至,卻和暖如春钉赁,著一層夾襖步出監(jiān)牢的瞬間蹄殃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工你踩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诅岩,地道東北人穷遂。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓道偷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親前塔。 傳聞我的和親對象是個殘疾皇子膝藕,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容