iOS架构模式-VIPER

相信大家如果读完这篇Architecting iOS Apps with VIPER(译),已经对iOS的VIPER架构模式有了一定了解。如果蒙蒙哒,没关系,那么这篇文章,哥们带你进一步认识VIPER。在这篇文章中我会对公司目前项目中的VIPER架构进行分解。同时你也可以去下载Demo

VIPER是通过单一责任原则进行的,所以如果大家在尝试VIPER架构模式中,遇到什么问题,记得上一篇文章中提到的,遵循此原则去解决问题。

保持一个类单一责任,它使类更强大。
单一责任原则规定,每个模块或类应该对软件提供的功能的单一部分负责,并且该责任应完全由类封装。 它的所有服务都应该与这一责任严格一致。 罗伯特·马丁表示原则如下:“A class should have only one reason to change”。

Main Parts of VIPER

The main parts of VIPER are:

  • View: (视图) 显示Presenter告知的内容,并将用户输入中继回Presenter。

  • Interactor: (交互器)包含用例指定的业务逻辑。

  • Presenter: (表示层,也可称主持人)包含用于准备显示内容(如从Interactor接收的)和用于对用户输入做出反应(通过从Interactor请求新数据)的视图逻辑。

  • Entity: (实体)包含Interactor使用的基本模型对象。

  • Routing: (路由)包含用于描述按哪个顺序显示哪些屏幕的导航逻辑。

这种分离也符合单一责任原则。 Interactor负责业务分析师,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)、RouterInteractor

同时View(ViewController)以及Interactor输出是通过Presenter完成的

Presenter

Presenter: (表示层,也可称主持人)包含用于准备显示内容(如从Interactor接收的)和用于对用户输入做出反应(通过从Interactor请求新数据)的视图逻辑。

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 拥有ViewRouterInteractor, data.

同时实现了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实现Interactor的接收数据输出协议,通过此行为将自己的dataObervable进行赋值.

presenter实现view的刷新数据输出协议,实现此协议的过程中调用了interactor的提供数据的输出行为

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订阅中,调用View的输入行为. (View的输入协议在View(ViewControler)中实现,刷新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主要由驱动UI的逻辑组成。 它知道何时呈现用户界面。 它从用户交互收集输入,以便它可以更新UI并将请求发送到Interactor。

Presenter从Interactor接收结果,并将结果转换为在View中有效显示的窗体。

Entities从不从Interactor传递给Presenter,Presenter只能准备要在View中显示的数据。

View(ViewController)

View: (视图) 显示Presenter告知的内容,并将用户输入中继回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告知的内容

HomeViewOutput将用户输入中继回Presenter(loadView中调用output.viewIsReady())

Interact

它包含了操作模型对象(Entities)来执行特定任务的业务逻辑。

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)。
如果你的实体只是数据结构。 任何与应用程序相关的逻辑很可能在交互器中。

Routing: (路由)

包含用于描述按哪个顺序显示哪些屏幕的导航逻辑。


20170216 未完待续....

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

推荐阅读更多精彩内容