iOS架構模式-VIPER

相信大家如果讀完這篇Architecting iOS Apps with VIPER(譯),已經(jīng)對iOS的VIPER架構模式有了一定了解差凹。如果蒙蒙噠,沒關系,那么這篇文章遮婶,哥們帶你進一步認識VIPER锦援。在這篇文章中我會對公司目前項目中的VIPER架構進行分解猛蔽。同時你也可以去下載Demo

VIPER是通過單一責任原則進行的,所以如果大家在嘗試VIPER架構模式中曼库,遇到什么問題区岗,記得上一篇文章中提到的,遵循此原則去解決問題。

保持一個類單一責任毁枯,它使類更強大慈缔。
單一責任原則規(guī)定,每個模塊或類應該對軟件提供的功能的單一部分負責种玛,并且該責任應完全由類封裝藐鹤。 它的所有服務都應該與這一責任嚴格一致。 羅伯特·馬丁表示原則如下:“A class should have only one reason to change”赂韵。

Main Parts of VIPER

The main parts of VIPER are:

  • View: (視圖) 顯示Presenter告知的內(nèi)容娱节,并將用戶輸入中繼回Presenter。

  • Interactor: (交互器)包含用例指定的業(yè)務邏輯祭示。

  • Presenter: (表示層肄满,也可稱主持人)包含用于準備顯示內(nèi)容(如從Interactor接收的)和用于對用戶輸入做出反應(通過從Interactor請求新數(shù)據(jù))的視圖邏輯。

  • Entity: (實體)包含Interactor使用的基本模型對象质涛。

  • Routing: (路由)包含用于描述按哪個順序顯示哪些屏幕的導航邏輯稠歉。

這種分離也符合單一責任原則。 Interactor負責業(yè)務分析師汇陆,Presenter代表交互設計師轧抗,而View負責視覺設計師。

不同組件及其連接方式的圖表

項目中的Modules目錄
HomeUI

從HomeConfigurator.swift 看VIPER架構各個功能模塊的交互

class HomeModuleConfigurator {

    func configureModuleForViewInput<UIViewController>(viewInput: UIViewController) {

        if let viewController = viewInput as? HomeViewController {
            configure(viewController: viewController)
        }
    }

    private func configure(viewController: HomeViewController) {

        let presenter = HomePresenter()
        let router = HomeRouter()
        let interactor = HomeInteractor()

        presenter.view = viewController
        presenter.router = router
        presenter.interactor = interactor

        interactor.output = presenter
        viewController.output = presenter
    }
}

Presenter包含View(ViewController)瞬测、Router横媚、Interactor

同時View(ViewController)以及Interactor輸出是通過Presenter完成的

Presenter

Presenter: (表示層,也可稱主持人)包含用于準備顯示內(nèi)容(如從Interactor接收的)和用于對用戶輸入做出反應(通過從Interactor請求新數(shù)據(jù))的視圖邏輯月趟。

import RxSwift

class HomePresenter {

    // V灯蝴、I、R
    weak var view: HomeViewInput!
    var interactor: HomeInteractorInput!
    var router: HomeRouterInput!

    // data
    var bannerObservable: Observable<Banner>!
    var coursesObservable: Observable<Courses>!

    // disposebag
    let disposebag = DisposeBag()
}


extension HomePresenter: HomeViewOutput {
    func viewIsReady() {
        reloadData()
    }

    func reloadData() {
        interactor.provideBannerData(path: "app-home-carousel")
        interactor.provideWikiData(department: 2, categoryId: "54611")

        bannerObservable
            .flatMap {banner -> Observable<Courses> in
                self.view.refreshBanner(banner: banner)
                return self.coursesObservable
            }
            .subscribe(onNext: { (wiki) in
                if let wikiResult = wiki.result {
                    if let wikiData = wikiResult.data {
                        if let wikiItem = wikiData.first {
                            self.view.refreshWiki(course: wikiItem)
                        }
                    }
                }
            }, onError: { (error) in
                self.view.loadDataSuccess()
                print("onError I found \(error)!")
            }, onCompleted: {
                self.view.loadDataSuccess()
                print("onCompleted")
            }).addDisposableTo(disposebag)
    }
}

extension HomePresenter: HomeInteractorOutput {
    func receiveBannerData(bannerObservable: Observable<Banner>) {
        self.bannerObservable = bannerObservable
    }

    func receiveWikiData(coursesObservable: Observable<Courses>) {
        self.coursesObservable = coursesObservable
    }
}

presenter 擁有View孝宗、Router穷躁、Interactor, data.

同時實現(xiàn)了HomeViewOutput以及 HomeInteractorOutput.

HomeViewOutput.swift

protocol HomeViewOutput {

    /**
        @author xijinfa
        Notify presenter that view is ready
    */

    func viewIsReady()

    func reloadData()
}

HomeInteractorOutput.swift

import Foundation
import RxSwift

protocol HomeInteractorOutput: class {
        func receiveBannerData(bannerObservable: Observable<Banner>)
        func receiveWikiData(coursesObservable: Observable<Courses>)
}

presenter實現(xiàn)Interactor的接收數(shù)據(jù)輸出協(xié)議,通過此行為將自己的dataObervable進行賦值.

presenter實現(xiàn)view的刷新數(shù)據(jù)輸出協(xié)議因妇,實現(xiàn)此協(xié)議的過程中調(diào)用了interactor的提供數(shù)據(jù)的輸出行為

interactor.provideBannerData(path: "app-home-carousel")
interactor.provideWikiData(department: 2, categoryId: "54611")

HomeInteractorInput.swift

protocol HomeInteractorInput {
    func provideBannerData(path: String)
    func provideWikiData(department: Int, categoryId: String)
}

在對dataObservable訂閱中问潭,調(diào)用View的輸入行為. (View的輸入?yún)f(xié)議在View(ViewControler)中實現(xiàn),刷新UI婚被。)

self.view.refreshBanner(banner: banner)
self.view.refreshWiki(course: wikiItem)
self.view.loadDataSuccess()

HomeViewInput.swift

protocol HomeViewInput: class {

    /**
        @author xijinfa
        Setup initial state of the view
    */

    func setupInitialState()

    func refreshBanner(banner: Banner)

    func refreshWiki(course: CourseData)

    func loadDataSuccess()
}

Presenter主要由驅(qū)動UI的邏輯組成狡忙。 它知道何時呈現(xiàn)用戶界面。 它從用戶交互收集輸入址芯,以便它可以更新UI并將請求發(fā)送到Interactor灾茁。

Presenter從Interactor接收結果窜觉,并將結果轉(zhuǎn)換為在View中有效顯示的窗體。

Entities從不從Interactor傳遞給Presenter北专,Presenter只能準備要在View中顯示的數(shù)據(jù)禀挫。

View(ViewController)

View: (視圖) 顯示Presenter告知的內(nèi)容,并將用戶輸入中繼回Presenter拓颓。

//
//  HomeHomeViewController.swift
//  xjf-ios-mvvm
//
//  Created by xijinfa on 18/01/2017.
//  Copyright ? 2017 xijinfa. All rights reserved.
//

import UIKit
import PullToRefresh

final class HomeViewController: UIViewController {

    // MARK: Properties

    var output: HomeViewOutput!

    private let refresher = PullToRefresh()

    fileprivate lazy var carsouselView: CarouselViewController = {
        return CarouselViewController(path: "app-dept3-carousel")
    }()

    fileprivate lazy var wikiCardView: WikiCardView = {
        return WikiCardView()
    }()

    fileprivate lazy var scrollView: UIScrollView = {
        return UIScrollView()
    }()


    // MARK: Life cycle

    override func loadView() {
        super.loadView()

        view.backgroundColor = UIColor.HexRGB(rgbValue: 0xf5f5f5)

        func addSubviews() {
            view.addSubview(scrollView)
            scrollView.addSubview(carsouselView.view)
            scrollView.addSubview(wikiCardView)
        }

        func configViews() {
            let homeConfigurator = HomeModuleConfigurator()
            homeConfigurator.configureModuleForViewInput(viewInput: self)

            let carsouselConfigurator = CarouselModuleConfigurator()
            carsouselConfigurator.configureModuleForViewInput(viewInput: carsouselView)
        }

        func layoutSubViews() {
            let screenWidth = UIScreen.main.bounds.width
            let screenHeight = UIScreen.main.bounds.height
            let statusBarHeight = UIApplication.shared.statusBarFrame.height

            scrollView.frame = CGRect(x: 0, y: statusBarHeight, width: screenWidth, height: screenHeight)
            scrollView.contentSize = CGSize(width: 0, height: screenHeight + 1)

            carsouselView.view.snp.makeConstraints { make in
                make.width.equalTo(scrollView.snp.width)
                make.height.equalTo(160)
                make.top.equalTo(scrollView)
            }

            wikiCardView.snp.makeConstraints { make in
                make.width.equalTo(scrollView.snp.width)
                make.height.equalTo(333)
                make.top.equalTo(carsouselView.view.snp.bottom).offset(10)
            }
        }

        func setupPullToRefresh() {
            scrollView.addPullToRefresh(refresher) { [weak self] in
                print("PullToRefresh")
                func reloadData() {
                    self?.output.reloadData()
                }
                reloadData()
            }
        }

        addSubviews()

        layoutSubViews()

        configViews()

        setupPullToRefresh()

        output.viewIsReady()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        func initNaviBar() {
            if let naviVC = self.navigationController {
                naviVC.setNavigationBarHidden(true, animated: false)
            }
        }
        initNaviBar()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    deinit {
        if let topPullToRefresh = scrollView.topPullToRefresh {
            scrollView.removePullToRefresh(topPullToRefresh)
        }
    }
}

extension HomeViewController: HomeViewInput {
    func setupInitialState() {

    }

    func refreshBanner(banner: Banner) {
        Logger.logInfo(message: "refresh banner")
        carsouselView.setBanner(banner: banner)
    }

    func refreshWiki(course: CourseData) {
        Logger.logInfo(message: "refresh wiki")
        wikiCardView.setData(courseData: course)
    }

    func loadDataSuccess() {
        Logger.logInfo(message: "load data success")
        scrollView.endRefreshing(at: Position.top)
    }
}

HomeViewInput顯示Presenter告知的內(nèi)容

HomeViewOutput將用戶輸入中繼回Presenter(loadView中調(diào)用output.viewIsReady())

Interact

它包含了操作模型對象(Entities)來執(zhí)行特定任務的業(yè)務邏輯语婴。

class HomeInteractor {
    weak var output: HomeInteractorOutput!
}

extension HomeInteractor: HomeInteractorInput {
    func provideBannerData(path: String) {
        self.output.receiveBannerData(bannerObservable: DataManager.getBanner(path: path))
    }

    func provideWikiData(department: Int, categoryId: String) {
        var params = Dictionary<String, String>()
        params.updateValue(categoryId, forKey:"category_id")
        self.output.receiveWikiData(coursesObservable: DataManager.getCourses(department: department, params: params))
    }
}

Entity (實體)

實體是由交互器操作的模型對象。 實體僅由交互器操縱驶睦。 交互器從不將實體傳遞到表示層(即Presenter)腻格。
如果你的實體只是數(shù)據(jù)結構。 任何與應用程序相關的邏輯很可能在交互器中啥繁。

Routing: (路由)

包含用于描述按哪個順序顯示哪些屏幕的導航邏輯菜职。


20170216 未完待續(xù)....

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市旗闽,隨后出現(xiàn)的幾起案子酬核,更是在濱河造成了極大的恐慌,老刑警劉巖适室,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嫡意,死亡現(xiàn)場離奇詭異,居然都是意外死亡捣辆,警方通過查閱死者的電腦和手機蔬螟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汽畴,“玉大人旧巾,你說我怎么就攤上這事∪绦” “怎么了鲁猩?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長罢坝。 經(jīng)常有香客問我廓握,道長,這世上最難降的妖魔是什么嘁酿? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任隙券,我火速辦了婚禮,結果婚禮上闹司,老公的妹妹穿的比我還像新娘娱仔。我一直安慰自己,他們只是感情好开仰,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布拟枚。 她就那樣靜靜地躺著薪铜,像睡著了一般众弓。 火紅的嫁衣襯著肌膚如雪恩溅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天谓娃,我揣著相機與錄音脚乡,去河邊找鬼。 笑死滨达,一個胖子當著我的面吹牛奶稠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捡遍,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼锌订,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了画株?” 一聲冷哼從身側(cè)響起辆飘,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谓传,沒想到半個月后蜈项,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡续挟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年紧卒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诗祸。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡跑芳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出直颅,到底是詐尸還是另有隱情聋亡,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布际乘,位于F島的核電站坡倔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏脖含。R本人自食惡果不足惜罪塔,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望养葵。 院中可真熱鬧征堪,春花似錦、人聲如沸关拒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谐算,卻和暖如春熟尉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背洲脂。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工斤儿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人恐锦。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓往果,卻偏偏與公主長得像,于是被迫代替她去往敵國和親一铅。 傳聞我的和親對象是個殘疾皇子陕贮,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

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