积木法搭建 iOS 应用—— VIPER
Posted 搜狐技术产品
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了积木法搭建 iOS 应用—— VIPER相关的知识,希望对你有一定的参考价值。
点击蓝字关注我们
本文字数:8391字
预计阅读时间:23分钟
在我们构建应用产品的时候,产品的快速发展也迫使我们不断寻求更合适产品高速迭代发展的编程架构。
伴随着产品的发展,让产品每一个部分容易被识别,拥有明显特定的目的,并且与其他部分逻辑清晰、结构明确是我们一直探寻的目标。
想必大家已经对经常使用的MVC、MVP、MVVM非常熟悉了,在本文中我们将探索 VIPER 架构在 ios 上的成功实践。
我们先大致了解什么是 VIPER。
VIPER 分为五个部分:View、Interactor、Presenter、Entity、Router。
View:视图部分,根据 Presenter 的要求展示界面。
Interactor:业务相关逻辑、从本地和网络获取数据,并存储数据。
Presenter:包含为显示做准备工作的相关视图逻辑(从 Interactor 接收数据,并进一步处理为 View 可以直接展示的数据),并对用户输入进行反馈(根据用户操作对当前数据做变更)。
Entity:包含 Interactor 要使用的基本模型对象。
Router:包含用来描述屏幕显示 view 和显示顺序的导航逻辑。
这种功能划分形式遵循单一职责原则。Interactor 负责业务分析获取内容的部分,Presenter 代表交互设计师为 View 展示做准备,而 View 相当于视觉设计师只负责展示内容,Entity 负责承载数据内容, Router 负责页面模块的显示和导航逻辑。
我们可以把他们之间关系画为下图:
VIPER 的每一个部分的创建、功能实现没有先后顺序,可以根据实际情况调整。
由于遵循职责单一,每一个部分也都可以拿出来给有相同功能的业务使用,
比如狐友APP中的关注、粉丝页面:
VIPER 的每一个部分就像是房子的梁、柱、墙以及装修材料,我们可以通过把形状、特点相同的结构重复利用搭建在不同的位置上,从而构建出我们想要的漂亮房子。
这种感觉是不是像极了我们小时候玩积木的样子?
下面我们来写一个推荐电影的列表,根据这个例子更深入的探索如何创建 VIPER 架构应用。
首先,我们针对各个部分的关系和功能定义通用协议,就像拼装日式木质结构的房子需要先有标准部件结构,再将标准部件结构组装起来一样,我们需要先构建 VIPER 的基础构件。
其次,后面我们会用这些基础构件搭建我们需要的业务逻辑。
基础构件
01
Router
Router 用来描述屏幕显示 View 和显示顺序的导航逻辑。在 VIPER 中我们把 viewController 看做是 View 的一部分,只做 view 的显示控制及用户操作反馈,不实际处理数据逻辑。
这里我们定义了可以获取设置 viewController 的属性。
/// Describes router component in a VIPER architecture.
protocol RouterType: class
/// The reference to the view which the router should use
/// as a starting point for navigation. Injected by the builder.
var viewController: UIViewController? get set
Interactor
/// Describes interactor component in a VIPER architecture.
protocol InteractorType: class
Presenter
Presenter 从 Interactor 接收数据做显示准备相关的处理后交给相关视图;并且对用户输入进行反馈,如果需要更新数据时通知 interactor 获取新的数据。
这里我们定义了 InteractorType 类型的 interactor 属性。
/// Describes presenter component in a VIPER architecture.
protocol PresenterType: class
associatedtype I: InteractorType
/// A interactor
var interactor: I get
View
View 根据 Presenter 的要求展示界面,所以我们定义刷新视图的方法以及一个遵守 PresenterType 的 presenter。
protocol ViewType
associatedtype P: PresenterType
/// A presenter
var presenter: P get
// MARK: - refresh View
func refreshView()
现在我们已经搭建好了Router、View、Presenter、Interactor之间的单向关系,如下图:
接下来,我们使用协议来完成各个模块之间的数据流动和用户行为反馈。
ListDataProtocol
由于应用程序中大部分页面都是列表,所以我们对列表也做一些通用的功能处理,减少业务层的重复逻辑。
我们的列表数据需要有 row 和 section ,我们需要定义行和组一些显示需要的通用信息:
protocol ViewModelType
var cellId: String get
var cellSize: CGSize get
protocol SectionType
var items: [ViewModelType] get set
var headerSize: CGSize get
var footerSize: CGSize get
var headerId: String get
var footerId: String get
var headerTitle: String get
var footerTitle: String get
class Section: SectionType
var items: [ViewModelType] = []
var headerSize: CGSize = CGSize.zero
var footerSize: CGSize = CGSize.zero
var headerId: String = ""
var footerId: String = ""
var headerTitle: String = ""
var footerTitle: String = ""
protocol ListDataProtocol: class
// MARK: -
// MARK: - Data information
var viewModels: [Section] get set
func numberOfSections() -> Int
func numberOfItemsInSection(at index: Int) -> Int
func item(at indexPath: IndexPath) -> ViewModelType?
func section(in index: Int) -> Section?
// MARK: -
// MARK: - legitimacy
func indexPathAccessibleInViewModels(_ indexPath: IndexPath) -> Bool
indexPathAccessibleInViewModels: 方法接受一个 indexPath ,并且返回这个 indexPath 是否在当前 viewModels 中可以访问的布尔值,以便我们减少重复书写判断数组越界的逻辑。
extension ListDataProtocol
// MARK: -
// MARK: - Data information
func numberOfSections() -> Int
return self.viewModels.count
func numberOfItemsInSection(at index: Int) -> Int
#if DEBUG
assert(index < self.viewModels.count, "Index out of bounds exception")
#else
#endif
return section(in: index)?.items.count ?? 0
extension ListDataProtocol
func item(at indexPath: IndexPath) -> ViewModelType?
if indexPathAccessibleInViewModels(indexPath) == false
return nil
return self.viewModels[indexPath.section].items[indexPath.row]
func section(in index: Int) -> Section?
#if DEBUG
assert(index < self.viewModels.count, "Index out of bounds exception")
#else
#endif
if index >= self.viewModels.count
return nil
return self.viewModels[index]
extension ListDataProtocol
// MARK: -
// MARK: - legitimacy
func indexPathAccessibleInViewModels(_ indexPath: IndexPath) -> Bool
#if DEBUG
assert(indexPath.section < self.viewModels.count, "Index out of bounds exception ( please check indexPath.section)")
assert(indexPath.row < self.viewModels[indexPath.section].items.count, "Index out of bounds exception (please check indexPath.row)")
#else
#endif
if indexPath.section >= self.viewModels.count ||
indexPath.row >= self.viewModels[indexPath.section].items.count
return false
return true
protocol ListDataProtocol: class
// MARK: -
// MARK: - Data manipulation
/// Retrieve data from memory
func updateSection(section: Section, at index: Int)
func updateItem(item: ViewModelType, at indexPath: IndexPath)
通常实现相同,添加默认实现如下:
extension ListDataProtocol
// MARK: -
// MARK: - Data manipulation
/// Retrieve data from memory
func updateSection(section: Section, at index: Int)
#if DEBUG
assert(index < self.viewModels.count, "Index out of bounds exception")
#else
#endif
if index >= self.viewModels.count
return
self.viewModels[index] = section
func updateItem(item: ViewModelType, at indexPath: IndexPath)
guard indexPathAccessibleInViewModels(indexPath) else
return
self.viewModels[indexPath.section].items[indexPath.row] = item
协议定义:
protocol ListDataProtocol: class
/// Insert data
func insertSection(section: Section, at index: Int)
func insertItem(item: ViewModelType, at indexPath: IndexPath)
extension ListDataProtocol
/// Insert data
func insertSection(section: Section, at index: Int)
#if DEBUG
assert(index <= self.viewModels.count, "Index out of bounds exception")
#else
#endif
if index > self.viewModels.count
return
self.viewModels.insert(section, at: index)
func insertItem(item: ViewModelType, at indexPath: IndexPath)
#if DEBUG
assert(indexPath.section <= self.viewModels.count, "Index out of bounds exception (indexPath.section)")
assert(indexPath.row <= self.viewModels[indexPath.section].items.count, "Index out of bounds exception (indexPath.row)")
#else
#endif
if indexPath.section > self.viewModels.count ||
indexPath.row > self.viewModels[indexPath.section].items.count
return
self.viewModels[indexPath.section].items.insert(item, at: indexPath.row)
协议定义:
protocol ListDataProtocol: class
/// Delete data
func deleteSection(at index: Int)
func deleteItem(at indexPath: IndexPath)
通常实现相同,默认实现如下:
extension ListDataProtocol
/// Delete data
func deleteSection(at index: Int)
#if DEBUG
assert(index < self.viewModels.count, "Index out of bounds exception")
#else
#endif
if index >= self.viewModels.count
return
self.viewModels.remove(at: index)
func deleteItem(at indexPath: IndexPath)
guard indexPathAccessibleInViewModels(indexPath) else
return
self.viewModels[indexPath.section].items.remove(at: indexPath.row)
// 协议定义
protocol ListDataProtocol: class
/// Clear all data
func clearList()
// 协议实现
extension ListDataProtocol
/// Clear all data
func clearList()
self.viewModels = []
ListViewProtocol
protocol ListViewProtocol
// MARK: - load
func pulldown()
func loadMore()
// MARK: - register
func registerCellClass() -> [AnyClass]
func registerCellNib() -> [AnyClass]
func registerHeaderClass() -> [AnyClass]
func registerHeaderNib() -> [AnyClass]
func registerFooterClass() -> [AnyClass]
func registerFooterNib() -> [AnyClass]
// MARK: - refresh
func setUpRefreshHeader()
func setUpRefreshFooter()
列表的数据是由协议类型 ListDataProtocol 提供,UICollectionView 及 UITableView 的数据代理方法不能写在有泛型的协议中实现,所以我们需要一个实现含有 UICollectionView 或者 UITableView 属性的类。
它就是我们上面提到的 ViewType 协议类型,充当 VIPER 中 view 的角色。
现在我们完成了VIPER 中 View 根据用户操作向 Presenter 索要数据,Presenter 向 view提供显示所需的数据支持,我们需要一个列表 View 去显示 Presenter提供的数据,这就是我们接下来讲的 VTableViewController。
VTableViewController
下面我们实现拥有 UITableView 的 Controller。Controller 从 presenter 获取展示需要的数据直接展示在界面上。
VTableViewController 的 presenter 为视图提供数据的支持,presenter 遵守 PresenterType & ListDataProtocol 两个协议。为了业务层灵活实现 tableView,这里 tableView 是一个泛型:
/// Viper view controller base class.
typealias ListPresenterType = PresenterType & ListDataProtocol
class VTableViewController<P: ListPresenterType, T: UITableView>: UIViewController, ViewType
let presenter: P
init(presenter: P, style: UITableView.Style)
self.presenter = presenter
self.tableView = T.init(frame: CGRect.zero, style: style)
super.init(nibName: nil, bundle: nil)
self.view.backgroundColor = UIColor.white
// MARK: -
// MARK: - View life cycle
override func viewDidLoad()
super.viewDidLoad()
hy_setUpUI()
// MARK: -
// MARK: - tableView
var tableView: T
private func hy_setUpUI()
self.view.addSubview(self.tableView)
self.tableView.frame = self.view.bounds
self.tableView.dataSource = self
self.tableView.delegate = self
// MARK: -
// MARK: - viewType
func refreshView()
self.tableView.reloadData()
class VTableViewController<P: ListPresenterType, T: UITableView>: UIViewController, ViewType, ListViewProtocol
// MARK: -
// MARK: - ListViewProtocol
func pulldown()
func loadMore()
func setUpRefreshHeader()
func setUpRefreshFooter()
具体业务中还需要实现注册视图的方法,在 VTableViewController 中我们只增加空实现,如下:
class VTableViewController<P: ListPresenterType, T: UITableView>: UIViewController, ViewType, ListViewProtocol
// MARK: -
// MARK: - ListViewProtocol
func registerCellClass() -> [AnyClass] return []
func registerCellNib() -> [AnyClass] return []
func registerHeaderClass() -> [AnyClass] return []
func registerHeaderNib() -> [AnyClass] return []
func registerFooterClass() -> [AnyClass] return []
func registerFooterNib() -> [AnyClass] return []
我们需要根据上面注册类型方法返回类型对 VTableViewController 的 tableView 进行注册视图,实现如下:
class VTableViewController<P: ListPresenterType, T: UITableView>: UIViewController, ViewType, ListViewProtocol
// MARK: - private
private func hy_registeCell()
for cellClass in self.registerCellClass()
self.tableView.register(cellClass, forCellReuseIdentifier: NSStringFromClass(cellClass))
for cellClass in self.registerCellNib()
self.tableView.register(UINib.init(nibName: NSStringFromClass(cellClass), bundle: nil), forCellReuseIdentifier: NSStringFromClass(cellClass))
private func hy_registeHeaderAndFooterView()
let headerAndFooterClass = self.registerHeaderClass() + self.registerFooterClass()
for viewClass in headerAndFooterClass
self.tableView.register(viewClass, forHeaderFooterViewReuseIdentifier: NSStringFromClass(viewClass))
let headerAndFooterNib = self.registerHeaderNib() + self.registerFooterNib()
for viewClass in headerAndFooterNib
self.tableView.register(UINib.init(nibName: NSStringFromClass(viewClass), bundle: nil), forHeaderFooterViewReuseIdentifier: NSStringFromClass(viewClass))
我们需要在tableView创建之后注册复用view,所以需要更改前面 hy_setUpUI 方法为:
private func hy_setUpUI()
self.view.addSubview(self.tableView)
self.tableView.frame = self.view.bounds
self.tableView.dataSource = self
self.tableView.delegate = self
self.hy_registeCell()
self.hy_registeHeaderAndFooterView()
VTableViewController 需要根据 presenter 提供的数据显示列表视图部分,我们需要实现 UITableViewDelegate, UITableViewDataSource 两个协议,这个时候我们就需要用到 presenter 在 PresenterType 和 ListDataProtocol中定义的方法,从 presenter 中直接拿到可以用来展示的数据给视图展示。
我们接下来添加 UITableViewDataSource 相关的 cell 显示方法实现:
class VTableViewController<P: ListPresenterType, T: UITableView>: UIViewController, ViewType, ListViewProtocol, UITableViewDelegate, UITableViewDataSource
// MARK: -
// MARK: - tableView data source
func numberOfSections(in tableView: UITableView) -> Int
return self.presenter.numberOfSections()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return self.presenter.numberOfItemsInSection(at: section)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
let cellId = self.presenter.item(at: indexPath)?.cellId ?? ""
#if DEBUG
assert(self.presenter.item(at: indexPath) != nil, "There is no item")
assert(cellId.isEmpty != true, "Item don\'t has cellId")
#else
if cellId.isEmpty
return UITableViewCell.init()
#endif
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellId) else
return UITableViewCell.init()
return cell
通过上面的代码我们可以将 presenter 中已经准备好的数据交给 tableView 显示。
通常列表中除了 cell 的显示还有 sectionHeader、sectionFooter 的显示,我们依然通过 presenter 给的数据来显示这些视图:
class VTableViewController<P: ListPresenterType, T: UITableView>: UIViewController, ViewType, ListViewProtocol, UITableViewDelegate, UITableViewDataSource
// MARK: -
// MARK: - tableView data source
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
#if DEBUG
assert(self.presenter.section(in: section) != nil, "There is no section")
#endif
return self.presenter.section(in: section)?.headerTitle ?? ""
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
#if DEBUG
assert(self.presenter.section(in: section) != nil, "There is no section")
#endif
return self.presenter.section(in: section)?.footerTitle ?? ""
// MARK: -
// MARK: - tableView delegate
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
#if DEBUG
assert(self.presenter.section(in: section) != nil, "There is no section")
#endif
let headerId = self.presenter.section(in: section)?.headerId ?? ""
// No found header
guard let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerId) else
return nil
return header
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
#if DEBUG
assert(self.presenter.section(in: section) != nil, "There is no section")
#endif
let footerId = self.presenter.section(in: section)?.footerId ?? ""
// No found header
guard let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: footerId) else
return nil
return header
cell、sectionHeader、sectionFooter 还需要设置大小。
这里我们默认所有 view 都会被注册,在 release 版本中对于不能获取到复用 Id 的视图 size 将被设置为 0 ,它将不展示给用户。在 debug 版本中,我们将依然会展示此 View 以便及时发现问题,并更正错误。
所以现实代理方法如下:
class VTableViewController<P: ListPresenterType, T: UITableView>: UIViewController, ViewType, ListViewProtocol, UITableViewDelegate, UITableViewDataSource
// MARK: -
// MARK: - tableView delegate
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
#if DEBUG
assert(self.presenter.item(at: indexPath) != nil, "There is no item")
return self.presenter.item(at: indexPath)?.cellSize.height ?? 0
#else
guard let cellId = self.presenter.item(at: indexPath)?.cellId else
return 0
if cellId.isEmpty
return 0
// No found cell
let registeCells = registerCellClass() + registerCellNib()
guard (registeCells.contains NSStringFromClass($0) == cellId) else
return 0
return self.presenter.item(at: indexPath)?.cellSize.height ?? 0
#endif
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
#if DEBUG
assert(self.presenter.section(in: section) != nil, "There is no section")
return self.presenter.section(in: section)?.headerSize.height ?? 0
#else
// There is no headerId
guard let headerId = self.presenter.section(in: section)?.headerId else
return 0
if headerId.isEmpty
return 0
// No found header
let registeHeaders = registerHeaderClass() + registerHeaderNib()
guard (registeHeaders.contains NSStringFromClass($0) == headerId) else
return 0
return self.presenter.section(in: section)?.headerSize.height ?? 0
#endif
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
#if DEBUG
assert(self.presenter.section(in: section) != nil, "There is no section")
return self.presenter.section(in: section)?.footerSize.height ?? 0
#else
guard let footerId = self.presenter.section(in: section)?.footerId else
return 0
if footerId.isEmpty
return 0
// No found footer
let registeFooters = registerFooterClass() + registerFooterNib()
guard (registeFooters.contains NSStringFromClass($0) == footerId) else
return 0
return self.presenter.section(in: section)?.footerSize.height ?? 0
#endif
class HYTableViewCell<T>: UITableViewCell
var viewModel: T?
func setViewModel(_ viewModel: T)
self.viewModel = viewModel
iOS VIPER架构
洋葱模型
洋葱模型,是从冰山模型上演变而来的,用来进行层次分析的模型,这是Redux的洋葱模型。
action从最外层传入,层层传递直至核心后,经过逐层事件触发,再次被分发出来,执行后续操作。
洋葱模型如今已经广泛应用于各个领域,进行更直观清晰的分层剖析。
The Clean Architecture
Robert C·Martin是《Clean Code》的作者,我们习惯称他为Uncle Bob。2012年8月13日,他在他的个人Blog上,提出了著名的The Clean Architecture。
Uncle Bob利用洋葱模型,设计出了The Clean Architecture。
这种架构的核心在于依赖的规则:源码依赖只能指向内部,内圈的部分,并不知道关于外圈的任何事情,即内部不依赖外部,外部依赖于内部。一般来说,越深入则代表软件级别越高,外部是机制,而内部是策略。
The Clean Architecture可以给软件带来如下优点:
独立的框架:框架可作为工具,不与系统混在一起;
可测试:没有UI、数据库和Web服务器,数据也可以测试;
UI独立:UI改变更容易,而不会影响业务规则;
数据库独立:业务规则不会和数据库绑定,可以任意切换数据库;
外部代理独立:业务规则可以对外部世界无感知。
The Clean Architecture也符合领域驱动设计DDD(Domain Driven Design)的思想,代码即方案。DDD是30年来业务软件开发实践经验的结晶。在传统编码模式中,如MVC架构,久而久之会演变为Massive View Controller,即重量级视图控制器,代码也将变为“面条代码”,而后面所有维护的人员,都将陷入耦合和臃肿的代码泥潭中,被代码所蛊惑,无法自拔,重心离要解决的问题越来越远,越走越偏,所以我们更需要使用模型来交流。
DDD希望,即使不是开发人员,在参与开发的各方,也都能理解软件模型。
在The Clean Architecture中,将软件分为四个层次,即Entities、Use Cases、Interface Adapters、Frameworks and Drivers。
Entities:Enterprise Business Rules,业务实体;
Use Cases:Application Business Rules,封装和实现业务功能,负责实体的数据处理转换,与数据库和UI无关;
Interface Adapters:接口适配器层,如MVC中的Controllers,MVP中的Presenters;
Frameworks and Drivers:由UI、数据库框架、Web框架等框架和工具构成。
这种由外向内的结构在改进的MVP架构中,可以表现为View -> Presenter -> Interactor -> Entity,通过这些原则将系统进行分层,将会创建一个可测试的架构体系。
现在,The Clean Architecture已经成为了开发界的Vans。
这是2016年,在Android端实践过The Clean Architecture的开发者Dario Mili?i?的感受。
For me, this has been the best way to develop apps so far. Decoupled code makes it easy to focus your attention on specific issues without a lot of bloatware getting in the way. After all, I think this is a pretty SOLID approach but it does take some time getting used to.
他说,”对于我来说,这是目前最好的App开发方式。解耦的代码使你更容易将注意力集中在具体问题上,而不是被臃肿的代码所妨碍。我认为这是一个相当可靠的方法,但它需要一段时间去适应。”
MVP-Clean
Android Architecture Blueprints
The Clean Architecture也是MVP和MVVM都在追求的结构。
Google官方发布了安卓架构蓝图Android Architecture Blueprints,其中包括MVP + Clean Architecture,在MVP-Clean中,其在传统的MVP上,基于The Clean Architecture,对MVP做了改进。
MVP起源于20世纪90年代,最初用于C++底层编程,后来被迁移到Java中进而推广,旨在促进自动化单元测试。这是安卓架构蓝图的MVP模型,拥有基本的Model-View-Presenter结构。
经过Clean改进的安卓架构蓝图MVP,其洋葱模型如下所示:
它在UI层和存储层添加了Domain,将应用分为三层,如下图所示:
Domain层负责提供业务逻辑,Domain层和Use Cases上很完美的解决了代码重复的问题,即相同的业务逻辑可以使用Use Case进行复用。
Domain层的所有代码也可以进行单元测试,也可以扩展使用集成测试,覆盖了一些数据边界后,MVP-Clean架构可以很快地为App提供良好的自动化测试基础。
通过Domain层的Use Cases,模块的职能一目了然,这是符合DDD的思想的,即使是其他非开发人员参与到项目中,通过Use Cases也能快速清晰地明白每个模块都负责什么样的业务,拥有什么样的功能。
安卓架构蓝图中的Use Cases提供一个CallBack,包括数据处理成功和处理失败的回调,而Presenter负责根据回调后的结果,处理View的界面显示,Presenter控制器不再单一的控制View和Model,承担过重的业务逻辑,而相同的业务逻辑不能复用。MVP-Clean的Presenter因为有了Use Cases,使得自身更加干净,分离出来的业务逻辑和数据处理等由Use Cases承担,其他业务也能够良好复用。
VIPER
VIPER架构在2013年由Jeff Gilbert 和 Conrad Stoll 提出。其最初是为了解决测试编写的难题。因为MVC演变的Massive View Controller的结构,使得Controllers越来越大,而将业务逻辑纯粹转移到Presenter也使得Presenter担任同样过重的职责,业务逻辑也不能够很好的复用。VIPER是在一定程度上基于The Clean Architecture的思想后改进而来的方案。
使用VIPER架构改进,具有以下优点:
易于测试;
代码结构更清晰,符合DDD;
良好的分离关注点;
责任被划分的很清晰:做什么和怎么做。
VIPER中各个字母分别代表View、Interactor、Presenter、Entity和Router。
VIPER符合单一职责原则,Interactor负责控制业务逻辑,Presenter负责控制Interactor和View的交互,View负责设计展示UI。Presenter定义行为,而Interactor定义业务逻辑,各个组件连接结构关系可以用如下图表示:
这张图清晰表达了数据流和依赖流。View负责通知Presenter生命周期,Presenter向Interactor发送数据,请求数据处理,Interactor知道应该如何处理Entity,Presenter负责更新UI,而WireFrame负责跳转。
Interactor
Interactor层中主要包含各个业务中的Use Case,不包含任何UI相关的操作,这些Use Case通过Callback回调给Presenter状态,success或error,并传递响应的处理结果。
因为Interactor更纯粹,只涉及数据操作,所以其对TDD(测试驱动开发)更加友好。
Entity
实体负责定义数据结构,这里的实体并不像其他经典著作中,需要定义更多的数据处理的行为,在VIPER中,数据处理更多是由Interactor来完成,而Entity更加纯粹。
Presenter
Presenter承担的是When to do的职责。知道什么时候通知Interactor处理数据,知道什么时候通知View更新UI显示,也知道什么时候应该进行跳转。
Router
Router负责链接各个模块,Presenter知道何时跳转却不知道如何跳转,Router则承担How to do的职责进行各个模块之间的交互转向。
View
View负责展示界面,但不知自己应该何时展示,Presenter控制View的展示时机,View只负责绘制自身的表现,而将接口暴露出来,让Presenter控制其行为。
总结
从臭名昭著的MVC,到MVVM,数据绑定对测试和调试并不友好,而代码复用也存在问题,再到今天过重Presenter的MVP,代码复用仍然存在问题,而Presenter的职责也使得开发人员的重心过多沉溺于如何重构代码之上,甚至久而久之就演变为破窗理论,这些结构都不能够很完美的被接受。
The Clean Architecture是改善测试、代码混乱和代码复用问题的明星理论,众多权威人士都对其拥有极高的认可度,而VIPER在IOS系统已获得了更多的认可,VIPER也是对The Clean Architecture的比较美的诠释。
VIPER和MVP-Clean还是有非常多的相似点,MVP-Clean更像是VIPE架构,而缺少了一层Router。
如果对于组件化来说,Router这层是必不可少的,而MVP-Clean解决了代码复用和测试等问题,VIPER则将二者巧妙并自然地衔接起来,安静地谱写着一篇美妙的乐章。(本章节图片来自网络)
以上是关于积木法搭建 iOS 应用—— VIPER的主要内容,如果未能解决你的问题,请参考以下文章