积木法搭建 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 中可以访问的布尔值,以便我们减少重复书写判断数组越界的逻辑。
获取 row、section 数量:
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
}
}
获取 row、section 数据模型:
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]
}
}
判断 IndexPath 是否在当前 viewModels 中可以访问:
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
}
}
更新row、section 数据:
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
}
}
插入row、section数据:
协议定义:
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)
}
}
删除row、section数据:
协议定义:
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
}
}
到这里我们已经完成了 View 显示 Presenter 提供的数据,基础构件已经准备就绪,他们之间的关系结构如下:
产品实践
02
RecommendRouter
class RecommendRouter: RouterType {
var viewController: UIViewController?
override init() {
let presenter = RecommendPresenter.init()
let viewController = RecommendViewController.init(presenter: presenter, style: UITableView.Style.plain)
self.viewController = viewController
super.init()
}
}
Entity
class RecommendModel {
var name = ""
var image = ""
var brief = ""
}
class RecommendViewModel: ViewModelType {
var model:RecommendModel
init(_ model: RecommendModel) {
self.model = model
}
var cellId: String {
return NSStringFromClass(RecommendCell.self)
}
var cellSize: CGSize {
return CGSize.init(width: UIScreen.main.bounds.width, height: 180)
}
lazy var name: NSAttributedString = {
return NSAttributedString.init(string: "影片名:\(model.name)", attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 32, weight: UIFont.Weight.medium)])
}()
lazy var brief: NSAttributedString = {
return NSAttributedString.init(string: "简介:\(model.brief)", attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.regular),NSAttributedString.Key.foregroundColor: UIColor.gray])
}()
}
RecommendInteractor
Interactor 我们分为两部分功能,一部分是从网络获取数据,一部分是数据的本地化操作。这里我们分别定义DBManager 和 NetWorkManager.
DBManager 包括对本地数据的增删改查操作,这里暂时只添加获取及保存数据操作的伪代码。
struct DBManager {
static func saveListToDB(list: [Section]) {
// save RecommendModel in section
}
static func saveModelToDB(model: RecommendViewModel) {
// save RecommendViewModel.model
}
static func loadDBData() -> [Section] {
// get RecommendViewModels
return []
}
}
struct NetWorkManager {
static func requestData(completion:(_ success: Bool, _ list:[Section]) -> ()) {
let theAvengers_4 = RecommendModel.init()
theAvengers_4.name = "复仇者联盟4:终局之战"
theAvengers_4.image = "https://pic8.iqiyipic.com/image/20190715/5f/96/a_100302620_m_601_m1_195_260.jpg"
theAvengers_4.brief = "故事发生在灭霸消灭宇宙一半的生灵并重创复仇者联盟之后,剩余的英雄被迫背水一战,为22部漫威电影写下传奇终章。"
let viewModel_4 = RecommendViewModel.init(theAvengers_4)
let theAvengers_3 = RecommendModel.init()
theAvengers_3.name = "复仇者联盟3:无限战争"
theAvengers_3.image = "https://img9.doubanio.com/view/photo/l/public/p2517753454.jpg"
theAvengers_3.brief = "最先与灭霸军团遭遇的雷神索尔一行遭遇惨烈打击,洛基遇害,空间宝石落入灭霸之手。未几,灭霸的先锋部队杀至地球,一番缠斗后掳走奇异博士。为阻止时间宝石落入敌手,斯塔克和蜘蛛侠闯入了敌人的飞船。与此同时,拥有心灵宝石的幻视也遭到外星侵略者的袭击,为此美国队长、黑寡妇等人将其带到瓦坎达王国,向黑豹求助......"
let viewModel_3 = RecommendViewModel.init(theAvengers_3)
let theAvengers_2 = RecommendModel.init()
theAvengers_2.name = "复仇者联盟2:奥创纪元"
theAvengers_2.image = "https://img3.doubanio.com/view/photo/l/public/p2237747953.jpg"
theAvengers_2.brief = "托尼·斯塔克试图重启一个已经废弃的维和项目,不料该项目却成为危机导火索。世上最强大的超级英雄——钢铁侠、美国队长、雷神、绿巨人、黑寡妇和鹰眼 ,不得不接受终极考验,拯救危在旦夕的地球。"
let viewModel_2 = RecommendViewModel.init(theAvengers_2)
let section = Section.init()
section.items = [viewModel_4, viewModel_3, viewModel_2]
completion(true, [section])
}
}
RecommendPresenter
typealias LoadCompletion = (LoadFeedback) -> ()
typealias Completion = (Bool, LoadFeedback) -> ()
struct LoadFeedback {
var msg: String = ""
var needRefresh: Bool = true
var loadingState: LoadingState = .hidden
var hasMore = true
var footerText: String?
}
enum LoadingState {
case show
case hidden
}
enum LoadType {
case pulldown
case loadMore
}
class RecommendPresenter: ListPresenterType {
var viewModels: [Section] = []
let interactor = RecommendInteractor.init()
func loadList(loadType: LoadType, localCompletion: LoadCompletion, completion: @escaping Completion) {
// 当前没有展示数据,先使用本地数据
if (loadType == .pulldown && self.viewModels.count == 0) {
self.viewModels = interactor.loadDBData()
var loadFeedback = LoadFeedback.init()
if self.viewModels.count > 0 {
loadFeedback.loadingState = .show
}
localCompletion(loadFeedback)
}
// 请求数据
self.interactor.requestData { (success, list, msg, hasMore) in
// 请求未成功
if !success {
var loadFeedback = LoadFeedback.init()
if self.viewModels.count <= 0 {
loadFeedback.msg = msg
}
completion(false, loadFeedback)
return
}
// 请求成功
if loadType == .loadMore {
self.viewModels += list
} else {
self.viewModels = list
}
var loadFeedback = LoadFeedback.init()
loadFeedback.hasMore = hasMore
if self.viewModels.count <= 0 {
loadFeedback.msg = msg
}
completion(true, loadFeedback)
}
}
}
RecommendCell
class RecommendCell: HYTableViewCell<RecommendViewModel> {
lazy var titleLabel = UILabel.init()
lazy var briefLabel = UILabel.init()
lazy var pic = UIImageView.init()
override func setViewModel(_ viewModel: RecommendViewModel) {
super.setViewModel(viewModel)
self.backgroundColor = UIColor.white
self.titleLabel.attributedText = viewModel.name
self.briefLabel.attributedText = viewModel.brief
self.pic.sd_setImage(with: viewModel.imageUrl) { (image, error, cacheType, url) in
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let margin: CGFloat = 10
let imageTop: CGFloat = 50
let imageHeight: CGFloat = 127
let imageWidth: CGFloat = 90
self.titleLabel.frame = CGRect.init(x: margin, y: margin * 2, width: self.bounds.width, height: 15)
self.contentView.addSubview(self.titleLabel)
self.briefLabel.frame = CGRect.init(x: imageWidth + margin * 2, y: imageTop, width: self.bounds.width - imageWidth - margin * 3, height: imageHeight)
self.contentView.addSubview(self.briefLabel)
self.briefLabel.numberOfLines = 0
self.pic.frame = CGRect.init(x: margin, y: imageTop, width: imageWidth, height: imageHeight)
self.contentView.addSubview(self.pic)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
RecommendViewController
class RecommendViewController: VTableViewController<RecommendPresenter, UITableView> {
override init(presenter: P, style: UITableView.Style) {
super.init(presenter: presenter, style: style)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.setUpRefreshHeader()
self.setUpRefreshFooter()
pulldown()
}
override func setUpRefreshHeader() {
self.tableView.mj_header = MJRefreshStateHeader.init(refreshingBlock: {
self.pulldown()
})
}
override func setUpRefreshFooter() {
self.tableView.mj_footer = MJRefreshAutoGifFooter.init(refreshingBlock: {
self.loadMore()
})
}
override func pulldown() {
self.presenter.loadList(loadType: LoadType.pulldown, localCompletion: { (loadFeedback) in
self.view.loading = loadFeedback.loadingState
self.tableView.reloadData()
}) { (success, loadFeedback) in
if !success {
self.tableView.mj_footer.endRefreshing()
return
}
self.view.loading = loadFeedback.loadingState
if (loadFeedback.needRefresh) {
self.tableView.reloadData()
}
self.tableView.mj_footer.endRefreshing()
}
}
override func loadMore() {
self.presenter.loadList(loadType: .loadMore, localCompletion: { (loadFeedback) in
}) { (success, loadFeedback) in
if !success {
self.tableView.mj_footer.endRefreshing()
return
}
self.view.loading = loadFeedback.loadingState
if (loadFeedback.needRefresh) {
self.tableView.reloadData()
}
self.tableView.mj_footer.endRefreshing()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
guard let item = self.presenter.item(at: indexPath) as? RecommendViewModel else {
return cell
}
if let hy_cell = cell as? HYTableViewCell<RecommendViewModel> {
hy_cell.setViewModel(item)
}
return cell
}
override func registerCellClass() -> [AnyClass] {
return [RecommendCell.self]
}
}
🔺 总结
可测试性好。UI测试和业务逻辑测试可以各自单独进行。 易于迭代。各部分遵循单一职责,可以很明确地知道新的代码应该放在哪里。 隔离程度高,耦合程度低。一个模块的代码不容易影响到另一个模块。 易于团队合作。各部分分工明确,团队合作时易于统一代码风格,可以快速接手别人的代码。
一个模块内的类数量增加,代码量增加,在层与层之间需要花更多时间设计接口。 模块的初始化较为复杂,打开一个新的界面需要生成 View、Presenter、Interactor,并且设置互相之间的依赖关系。而 iOS 中缺少这种设置复杂初始化的原生方式。
加入搜狐技术作者天团
千元稿费等你来!
戳这里!☛