点击蓝字关注我们
本文字数: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
Interactor 它是获取特定的数据并且组织数据的第一步。它与业务逻辑紧密相连,与展示逻辑分离,可以有独立的测试用例,可以较好的使用 TDD(即 Test Driven Development) 进行开发。Interactor 中的工作应当独立于任何显示界面,Interactor 可以同时运用于不同设备类型的数据提供层。
为了保持 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 和 p ,我们需要定义行和组一些显示需要的通用信息:
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 }
}
我们定义了一些 row 和 p 的类型 id、size 以供列表使用。因为在实际业务中 ViewModelType 需要根据业务需求定义不同类型,供不同功能需求使用,但是 SectionType 的功能需求及实现大部分相同,所以我们只定义通用的 p 类型如下:
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 = ""
}
下面我们定义关于列表数据的协议,把上面的 row 和 p 组织起来为列表提供数据支持。这里定义协议包括:列表数据的数组、获取行和组的信息、判断一个 indexPath 是否是有效的。
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 p(in index: Int) -> Section?
// MARK: -
// MARK: - legitimacy
func indexPathAccessibleInViewModels(_ indexPath: IndexPath) -> Bool
}
indexPathAccessibleInViewModels: 方法接受一个 indexPath ,并且返回这个 indexPath 是否在当前 viewModels 中可以访问的布尔值,以便我们减少重复书写判断数组越界的逻辑。
上面方法的实现通常是相同的,我们写默认实现如下:
获取 row、p 数量:
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 p(in: index)?.items.count ?? 0
}
}
获取 row、p 数据模型:
extension ListDataProtocol {
func item(at indexPath: IndexPath) -> ViewModelType? {
if indexPathAccessibleInViewModels(indexPath) == false {
return nil
}
return self.viewModels[indexPath.p].items[indexPath.row]
}
func p(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]
}
}
判断 IndexPath 是否在当前 viewModels 中可以访问:
extension ListDataProtocol {
// MARK: -
// MARK: - legitimacy
func indexPathAccessibleInViewModels(_ indexPath: IndexPath) -> Bool {
#if DEBUG
assert(indexPath.p < self.viewModels.count, "Index out of bounds exception ( please check indexPath.p)")
assert(indexPath.row < self.viewModels[indexPath.p].items.count, "Index out of bounds exception (please check indexPath.row)")
#else
#endif
if indexPath.p >= self.viewModels.count ||
indexPath.row >= self.viewModels[indexPath.p].items.count {
return false
}
return true
}
}
由于我们经常需要对数据进行修改更新、数据持久化操作,所以在 ListDataProtocol 中定义数据处理的通用协议及实现如下:
更新row、p 数据:
协议定义:
protocol ListDataProtocol: class {
// MARK: -
// MARK: - Data manipulation
/// Retrieve data from memory
func updateSection(p: Section, at index: Int)
func updateItem(item: ViewModelType, at indexPath: IndexPath)
}
通常实现相同,添加默认实现如下:
extension ListDataProtocol {
// MARK: -
// MARK: - Data manipulation
/// Retrieve data from memo