XE2V 项目收获系列:
前言
在首个版本发布之后,我继续对 YLRefreshKit 进行改进,从而有了第二版。第二版新增了 NetworkManagerType 与 TargetType 两个协议和 TViewController 、 CViewController 与 SViewController 类,重构了 RefreshOperator 类并更新了 Refreshable 与 AutoRefreshable 协议,最终实现了刷新操作的自动化。现在,你不需要在 ViewModel 或 Controller 中再编写任何刷新代码,YLRefreshKit 会自动帮你完成刷新操作。
简介
YLRefreshKit 对如下部分做了规范定义:
-
Cell
藉由 Configurable 给它扩展了 configure(_:) 方法,你需要在此方法中配置 cell 。
-
Model
由 ModelType 协议规范。它有一个可选的 nextPage 属性,加载下一页数据时会用的到;一个只读的 data 属性,返回网络请求得来的被转化成模型的数据;一个只读的 pageablePropertyPath 属性,返回可分页属性的 keyPath ;它还有一些可选的类型属性,存储 cell 的类型,用以自动化注册及创建 cell 。
-
Target
由 TargetType 协议规范。它是一个枚举类型,枚举成员会有一些关联值,存储着进行网络请求会用的到的一些信息,如页数等。它有一个 isRefreshable 的布尔属性,用以判断一个页面是否可以进行刷新操作;它还有一个可变的 update(with:targetInfo:) 方法,用于更新自身。
-
NetworkManager
由 NetworkManagerType 协议规范。它有四个关联类型:Target 、Model 、E 和 R 。Target 遵循 TargetType 协议,表示将要请求的页面;Model 遵循 ModelType ,表示网络请求数据将要转化成的模型;E 遵循 Error 协议,表示错误;R 表示网络请求方法的返回类型,它可以是任意类型,比如,如果你使用 Moya 进行网络请求,你可以把返回值类型设为 Cancellable 。R 是可选的,即网络请求方法可以没有返回值。
-
RefreshOperator
由 OperatorType 协议规范。它勾连起 DataSource(一般就是一个 ViewModel)、NetworkManager 和 Target ,正是它调用 networkManager 发起了网络请求,并将返回的结果赋予 dataSource 的 model 。如果你需要对刷新错误进行处理或在刷新前后进行一些操作,请创建一个 RefreshOperator 的子类并实现相应的方法。
-
StateMachine
指有限状态机。根据维基百科的定义,它是“表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型”。在这里,它的作用是当发生刷新动作时调用 RefreshOperator 进行相关操作,然后改变刷新状态,最后调用 completionHandler 进行刷新后的一些处理。创建状态机需要提供一个遵循 OperatorType 协议的类型,对于刷新来说就是提供一个 RefreshOperator 或它的子类。
-
Controller
要想在 controller 中使用状态机,你需要让它遵循并实现 Refreshable 协议。此协议规定了 refreshableView 和 refreshStateMachine 两个属性及 bindRefreshStateMachine() 和 bindRefreshStateMachine(_:) 两个方法。因为在扩展中给出了两个方法的实现,所以要实现此协议只需定义两个属性即可。为方便使用,定义了 TViewController 、CViewController 及 SViewController 三个类,分别对应页面是 table view 、collection view 和 scroll view 的情况。你可以让你的 viewController 继承自它们中的某一个,在 viewDidLoad() 中进行一些自定义的操作,然后调用 refreshableView 的
setAutoRefresh(refreshStateMachine:) 方法设置自动刷新。
源码解读
提示:YLRefreshKit 依赖于 YLStateMachine 与 YLExtensions 库,开始之前请先阅读此系列的前两篇文章。
-
RefreshState
刷新状态机是有状态的,它的状态有哪些?稍微思索一下就会知道,刷新状态有五种:初始状态、刷新中、已结束刷新但还有更多数据、加载更多和已结束刷新而且没有更多数据了。当然,还有一种错误状态。这些状态中,只有刷新中和加载更多是过渡状态,其他都是稳定状态。于是,RefreshState 可以这样定义:
import YLStateMachine
enum RefreshState: StateType {
case initial
case refreshing
case paginated
case paging
case populated
case error(Error)
var isError: Bool {
switch self {
case .error:
return true
default:
return false
}
}
var stability: Stability {
switch self {
case .refreshing, .paging:
return .transitional
default:
return .stable
}
}
static var initialState: RefreshState {
return .initial
}
}
-
RefreshAction
接着是动作,刷新动作只有两种:pullToRefresh 和 loadingMore,对应的过渡状态分别为 refreshing 和 paging 。由此给出 RefreshAction 的定义:
import YLStateMachine
enum RefreshAction: ActionType {
case pullToRefresh
case loadingMore
var transitionState: RefreshState {
switch self {
case .pullToRefresh:
return .refreshing
case .loadingMore:
return .paging
}
}
}
-
RefreshOperator
刷新状态机的最后一部分是 RefreshOperator ,正是在这里,完成了请求数据的自动化处理。思考一下它需要哪些属性。首先,它要进行网络请求,所以会有一个 networkManager ;网络请求要有一个 target ,所以还要有一个 target 属性;最后,网络请求得来的数据会赋予 model ,谁的 model ?其实是谁的 model 并不重要,只要是一个有 model 作为属性的对象即可,称它为 dataSource 吧。有了上面的讨论,我们看看它的具体实现:
import YLStateMachine
class RefreshOperator<
DS: DataSourceType, NM: NetworkManagerType
>: OperatorType where DS.Model == NM.Model
{
var dataSource: DS
var networkManager: NM
var target: NM.Target
init(dataSource: DS, networkManager: NM, target: NM.Target) {
self.dataSource = dataSource
self.networkManager = networkManager
self.target = target
}
func startTransition(_ state: RefreshState) { }
func endTransition(_ state: RefreshState) { }
func transition(with action: RefreshAction, completion: @escaping (RefreshState) -> Void) {
target.update(with: action, targetInfo: dataSource.targetInfo)
networkManager.request(target: target) {
[unowned self] (result: Result<DS.Model, NM.E>) in
switch result {
case .success(let model):
switch action {
case .pullToRefresh:
self.dataSource.model = model
case .loadingMore:
self.dataSource.model![keyPath: model.pageablePropertyPath!] += model[keyPath: model.pageablePropertyPath!]
}
// 传递刷新状态
let state: RefreshState = (model.nextPage == nil) ? .populated : .paginated
completion(state)
case .failure(let error):
completion(self.errorHandling(error))
}
}
}
func errorHandling(_ error: Error) -> RefreshState {
// 错误处理
// ...
return .error(error)
}
}
让我们看看 transition(with:completion:) 方法里发生了什么:
在请求数据之前,先更新 target 。更新 target 的页数需要知道发生的动作是下拉刷新还是加载更多,也即需要一个 action 参数。同时,页面跳转时 target 也会发生变化,此时可能需要传递一些信息,所以要有一个 targetInfo 参数。
数据请求过来之后,要将其存入 model 中,这部份的操作是模式化的,从而可以直接写出来。再之后,需要将刷新后的状态传出去,以便状态机进行后续操作。
-
DataSourceType
DataSourceType 是拥有 model 属性的某个对象。除此之外,它还可以做什么?嗯,DataSourceType 在页面跳转时会用的到,可以用它来传递一些信息,即它还有一个 targetInfo 属性。
import YLExtensions
protocol DataSourceType {
associatedtype Model: ModelType
var model: Model? { get set }
var targetInfo: Any? { get set }
}
-
TargetType
TargetType 用来定义 target 。target 对应的页面也许能够刷新,也许不能够刷新,我们需要一个布尔属性进行判断,即 isRefreshable 。target 在刷新时会发生变化,所以需要有一个 mutating 的 update(with:targetInfo:) 方法。
综上,我们给出 TargetType 的定义:
import YLStateMachine
protocol TargetType: Hashable {
/// 是否能进行下拉刷新。注意,不是指是否遵循 Refreshable 协议。
var isRefreshable: Bool { get }
/// 更新 target
mutating func update(with action: RefreshAction, targetInfo: Any?)
}
extension TargetType {
var isRefreshable: Bool { true }
mutating func update(with action: RefreshAction, targetInfo: Any?) { }
}
-
NetworkManagerType
网络请求方法的模样我们在 RefreshOperator 中已经见到过,这里直接给出它的定义:
import YLExtensions
protocol NetworkManagerType {
associatedtype target: TargetType
associatedtype Model: ModelType
associatedtype E: Error
associatedtype R
@discardableResult
func request(target: Target, completion: @escaping (Result<Model, E>) -> Void) -> R
}
-
Refreshable
完成了刷新状态机,该如何使用它呢?考虑一下,一个可刷新的页面是什么样的。首先,这个页面会有一个可刷新的 scrollView ;其次,既然要使用刷新状态机,页面自然要有一个刷新状态机的属性;最后,页面需要绑定刷新状态机,以便刷新完成后进行重载的操作。所以,Refreshable 是这样的:
import UIKit
import YLStateMachine
public protocol Refreshable {
associatedtype DS: DataSourceType
associatedtype NM: NetworkManagerType where DS.Model == NM.Model
var refreshStateMachine: StateMachine<RefreshOperator<DS, NM>>! { get set }
var refreshableView: UIScrollView? { get set }
func bindRefreshStateMachine()
func bindRefreshStateMachine(_ completion: @escaping () -> Void)
}
extension Refreshable where Self: UIViewController {
func bindRefreshStateMachine() {
refreshStateMachine.completionHandler = { [weak self] in
guard
let self = self,
!self.refreshStateMachine.currentState.isError
else { return }
if let tableView = self.refreshableView as? UITableView {
tableView.separatorStyle = .singleLine
tableView.reloadData()
} else if let collectionView = self.refreshableView as? UICollectionView {
collectionView.reloadData()
}
}
}
func bindRefreshStateMachine(_ completion: @escaping () -> Void) {
refreshStateMachine.completionHandler = { [weak self] in
guard
let self = self,
!self.refreshStateMachine.currentState.isError
else { return }
if let tableView = self.refreshableView as? UITableView {
tableView.separatorStyle = .singleLine
tableView.reloadData()
} else if let collectionView = self.refreshableView as? UICollectionView {
collectionView.reloadData()
}
completion()
}
}
}
-
AutoRefreshable
让我们来实现自动刷新功能。有了上面的基础,自动刷新其实很容易实现,只需要在 header 和 footer 中分别给状态机传入 pullToRefresh 和 loadingMore 动作,并在结束后判断是否显示 footer 即可:
import YLPullToRefreshKit
import YLStateMachine
protocol AutoRefreshable {
func setAutoRefresh<DS: DataSourceType, NM: NetworkManagerType>(
refreshStateMachine: StateMachine<RefreshOperator<DS, NM>>
) where DS.Model == NM.Model
}
extension UIScrollView: AutoRefreshable {
func setAutoRefresh<DS: DataSourceType, NM: NetworkManagerType>(
refreshStateMachine: StateMachine<RefreshOperator<DS, NM>>
) where DS.Model == NM.Model {
if refreshStateMachine.operator.target.isRefreshable {
configRefreshHeader(container: self) { [unowned self] in
refreshStateMachine.trigger(.pullToRefresh) {
self.configRefreshFooter(refreshStateMachine: refreshStateMachine)
switch refreshStateMachine.currentState {
case .error:
// 出错了
self.switchRefreshHeader(to: .normal(.failure, 0.5))
case .paginated:
// 有下一页
self.switchRefreshHeader(to: .normal(.success, 0.5))
default:
// 没有下一页了
self.switchRefreshHeader(to: .normal(.success, 0.5))
self.switchRefreshFooter(to: .removed)
}
}
}
configRefreshFooter(refreshStateMachine: refreshStateMachine)
} else {
refreshStateMachine.trigger(.pullToRefresh)
}
}
func configRefreshFooter<DS: DataSourceType, NM: NetworkManagerType>(
refreshStateMachine: StateMachine<RefreshOperator<DS, NM>>
) where DS.Model == NM.Model {
configRefreshFooter(container: self) { [unowned self] in
refreshStateMachine.trigger(.loadingMore) {
switch refreshStateMachine.currentState {
case .populated:
// 没有下一页了
self.switchRefreshFooter(to: .removed)
default:
// 有下一页或是出错了
self.switchRefreshFooter(to: .normal)
}
}
}
}
}
-
TViewController
我们创建一个 TViewController ,当页面是一个 table view 页面时,你可以继承它来简化你的代码:
import UIKit
import YLExtensions
import YLStateMachine
class TViewController<DS: DataSourceType, NM: NetworkManagerType, RO: RefreshOperator<DS, NM>>: UIViewController, Refreshable where DS.Model == NM.Model {
var refreshableView: UIScrollView? = UITableView()
var refreshStateMachine: StateMachine<RefreshOperator<DS, NM>>!
convenience init(refreshOperator: RO) {
self.init()
refreshStateMachine = StateMachine(operator: refreshOperator)
bindRefreshStateMachine()
}
convenience init(refreshOperator: RO, afterRefreshed: @escaping () -> Void) {
self.init()
refreshStateMachine = StateMachine(operator: refreshOperator)
bindRefreshStateMachine(afterRefreshed)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let tableView = refreshableView as? UITableView else { return }
tableView.frame = view.bounds
tableView.separatorStyle = .none
// 防止出现多余的分割线
tableView.tableFooterView = UIView()
tableView.dataSource = refreshStateMachine.operator.dataSource as? UITableViewDataSource
if DS.Model.tCells != nil {
tableView.registerCells(with: DS.Model.tCells!)
}
if DS.Model.tNibs != nil {
tableView.registerNibs(with: DS.Model.tNibs!)
}
view.addSubview(tableView)
}
}
-
CViewController
类似的建立 collection view 页面 CViewController :
import UIKit
import YLExtensions
import YLStateMachine
class CViewController<DS: DataSourceType, NM: NetworkManagerType, RO: RefreshOperator<DS, NM>>: UIViewController, Refreshable where DS.Model == NM.Model {
var refreshableView: UIScrollView? = UICollectionView()
var refreshStateMachine: StateMachine<RefreshOperator<DS, NM>>!
convenience init(refreshOperator: RO) {
self.init()
refreshStateMachine = StateMachine(operator: refreshOperator)
bindRefreshStateMachine()
}
convenience init(refreshOperator: RO, afterRefreshed: @escaping () -> Void) {
self.init()
refreshStateMachine = StateMachine(operator: refreshOperator)
bindRefreshStateMachine(afterRefreshed)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let collection = refreshableView as? UICollectionView else { return }
collection.frame = view.bounds
collection.dataSource = refreshStateMachine.operator.dataSource as? UICollectionViewDataSource
if DS.Model.cCells != nil {
collection.registerCells(with: DS.Model.cCells!)
}
if DS.Model.cNibs != nil {
collection.registerNibs(with: DS.Model.cNibs!)
}
if DS.Model.headViews != nil {
collection.registerHeaders(with: DS.Model.headViews!)
}
if DS.Model.headerNibs != nil {
collection.registerHeaderNibs(with: DS.Model.headerNibs!)
}
if DS.Model.footerViews != nil {
collection.registerFooters(with: DS.Model.footerViews!)
}
if DS.Model.footerNibs != nil {
collection.registerFooterNibs(with: DS.Model.footerNibs!)
}
view.addSubview(collection)
}
}
-
SViewController
最后是 scroll view 页面:
import UIKit
import YLExtensions
import YLStateMachine
open class SViewController<DS: DataSourceType, NM: NetworkManagerType, RO: RefreshOperator<DS, NM>>: UIViewController, Refreshable where DS.Model == NM.Model {
public var refreshableView: UIScrollView? = UIScrollView()
public var refreshStateMachine: StateMachine<RefreshOperator<DS, NM>>!
public convenience init(refreshOperator: RO) {
self.init()
refreshStateMachine = StateMachine(operator: refreshOperator)
bindRefreshStateMachine()
}
public convenience init(refreshOperator: RO, afterRefreshed: @escaping () -> Void) {
self.init()
refreshStateMachine = StateMachine(operator: refreshOperator)
bindRefreshStateMachine(afterRefreshed)
}
open override func viewDidLoad() {
super.viewDidLoad()
refreshableView!.frame = view.bounds
refreshableView!.contentSize = view.frame.size
refreshableView!.showsVerticalScrollIndicator = false
refreshableView!.backgroundColor = .white
view.addSubview(refreshableView!)
}
}
使用
YLRefreshKit 事实上对项目的各个部分都有规定,所以使用它涉及到项目的方方面面。虽然使用它的步骤较多,但它实际上简化了项目流程,要想了解它如何在项目中使用,请查看它的 README 并下载 Demo 来学习。
下篇预告
一个项目中除了刷新,还包含场景路由,如何将它们结合起来?这是我的下一个库要解决的问题。
源码地址:YLRefreshKit