前言
在軟件開發(fā)中桨踪,架構(gòu)是至關重要的一部分老翘,就好比蓋房子需要基本的鋼筋石樁等骨架,常聽到的架構(gòu)有MVC
锻离、MVP
铺峭、MVVM
、VIPER
等纳账,其中逛薇,MVC
是我們最常用的軟件架構(gòu)模式,而蘋果的整個API框架都是使用MVC
作為架構(gòu)的疏虫,所以我們會看到一些iOS的API中有這些類:UIXXXViewController
、UIXXXView
,而現(xiàn)在比較興起的架構(gòu)當屬MVP
和MVVM
卧秘,我個人覺得這它們是非常相似的呢袱,但在之前我使用第三方函數(shù)式、響應式框架RxSwift
或ReactiveCocoa
去實現(xiàn)MVP
或MVVM
架構(gòu)時翅敌,我自認為羞福,MVP
中的Presenter
專注于事件、數(shù)據(jù)的轉(zhuǎn)換蚯涮,成為View
層及Model
層的一條流通管道治专,而MVVM
中的ViewModel
更像是一個裝有視圖顯示數(shù)據(jù)的,并帶有一些顯示邏輯處理的分層遭顶,然后我們可以將ViewModel
中的顯示數(shù)據(jù)與View
中的視圖進行響應式綁定(個人觀點张峰,若有誤,望各位糾正)棒旗。在現(xiàn)在的開發(fā)中喘批,我也是使用MVP
或MVVM
架構(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
再細分成三層芜辕,分別是View
、Interactor
块差、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
浸踩、Interactor
、Presenter
三層中形成了一個流動閉環(huán)统求,而在其他的某些架構(gòu)中检碗,如MVC
据块、MVP
、MVVM
后裸,它們的數(shù)據(jù)在中間層會有著雙向的流動瑰钮,VIPER
較它們而言冒滩,其更加約束了整個軟件的架構(gòu)微驶,每一層功能特定,數(shù)據(jù)的流向單一开睡,使得軟件在開發(fā)中對原架構(gòu)的高度切合因苹。
如何配置 VIPER
在對VIPER
架構(gòu)的實現(xiàn)中,我是基于@羅琦aidenluo的VIP
架構(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
奈梳、Interactor
、Presenter
三層解虱;在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
旺上、Interactor
、Presenter
三層糖埋,它們各自實現(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中的
View
是weak
弱引用類型现拒,因為在View
、Interactor
望侈、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
松蒜、Interactor
、Presenter
封裝成模型已旧。
// 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:)
方法劣欢,一般我們可以在AppDelegate
的application(_, 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)的方式:
- 模態(tài)跳轉(zhuǎn)
- 導航跳轉(zhuǎn)
- 根視圖切換
- 返回
其中价脾,根視圖切換是針對應用程序主窗口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)合體,我分別起名叫One
和Two
:
// MARK: - VIPERs
enum VIPERs: String {
case one
case two
}
extension VIPERs {
var identifier: String {
return self.rawValue
}
}
在前面說到沧卢,綁定器以及路由都是通過聯(lián)合體的標識符來唯一標識的蚁堤,所以這里我讓枚舉的原始值類型為字符串,并在擴展中添加了獲取標識符的方法搏恤。
針對不同的聯(lián)合體违寿,Request
湃交、Response
、ViewModel
有所不同藤巢,所以這里我們定義兩個聯(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ā)深浮,可以看到压怠,當兩個按鈕被用戶點擊時,Interactor
的refresh(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)設計靈感源于@羅琦aidenluo的VIP
架構(gòu)設計思想盹沈,在這里我也感謝大神的指點龄章,讓我對VIPER
架構(gòu)有著更深層的了解。
本人為iOS開發(fā)菜鳥一只襟诸,若文章中某些話語不嚴謹或出現(xiàn)技術性錯誤瓦堵,還請各位提點意見,也歡迎各位在評論區(qū)進行討論歌亲,在這里也祝大家冬日愉快~
文章中實例的Github鏈接:TanVIPER