相信大家如果读完这篇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
)、Router
、Interactor
同时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
拥有View
、Router
、Interactor
, 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 未完待续....
网友评论