程序员开发框架iOS Developer

iOS架构模式-VIPER

2017-02-17  本文已影响5260人  HunterDude

相信大家如果读完这篇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:

这种分离也符合单一责任原则。 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 未完待续....

上一篇 下一篇

猜你喜欢

热点阅读