【XE2V 项目收获系列】三、YLRefresh:有了它,你可以删除你的刷新代码了

2,786 阅读9分钟

XE2V 项目收获系列:

一、YLExtensions:让 UITableView 及 UICollectionView 更易用

二、YLStateMachine:一个简单的状态机

三、YLRefresh:有了它,你可以删除你的刷新代码了

四、Decomposer:一个面向协议的构架模式

五、面向协议编程与操作自动化──以实现刷新自动化为例

前言

在首个版本发布之后,我继续对 YLRefreshKit 进行改进,从而有了第二版。第二版新增了 NetworkManagerTypeTargetType 两个协议和 TViewControllerCViewControllerSViewController 类,重构了 RefreshOperator 类并更新了 RefreshableAutoRefreshable 协议,最终实现了刷新操作的自动化。现在,你不需要在 ViewModelController 中再编写任何刷新代码,YLRefreshKit 会自动帮你完成刷新操作。

简介

YLRefreshKit 对如下部分做了规范定义:

  • Cell

藉由 Configurable 给它扩展了 configure(_:) 方法,你需要在此方法中配置 cell

  • Model

ModelType 协议规范。它有一个可选的 nextPage 属性,加载下一页数据时会用的到;一个只读的 data 属性,返回网络请求得来的被转化成模型的数据;一个只读的 pageablePropertyPath 属性,返回可分页属性的 keyPath ;它还有一些可选的类型属性,存储 cell 的类型,用以自动化注册及创建 cell

  • Target

TargetType 协议规范。它是一个枚举类型,枚举成员会有一些关联值,存储着进行网络请求会用的到的一些信息,如页数等。它有一个 isRefreshable 的布尔属性,用以判断一个页面是否可以进行刷新操作;它还有一个可变的 update(with:targetInfo:) 方法,用于更新自身。

  • NetworkManager

NetworkManagerType 协议规范。它有四个关联类型:TargetModelERTarget 遵循 TargetType 协议,表示将要请求的页面;Model 遵循 ModelType ,表示网络请求数据将要转化成的模型;E 遵循 Error 协议,表示错误;R 表示网络请求方法的返回类型,它可以是任意类型,比如,如果你使用 Moya 进行网络请求,你可以把返回值类型设为 CancellableR 是可选的,即网络请求方法可以没有返回值。

  • RefreshOperator

OperatorType 协议规范。它勾连起 DataSource(一般就是一个 ViewModel)、NetworkManagerTarget ,正是它调用 networkManager 发起了网络请求,并将返回的结果赋予 dataSourcemodel 。如果你需要对刷新错误进行处理或在刷新前后进行一些操作,请创建一个 RefreshOperator 的子类并实现相应的方法。

  • StateMachine

指有限状态机。根据维基百科的定义,它是“表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型”。在这里,它的作用是当发生刷新动作时调用 RefreshOperator 进行相关操作,然后改变刷新状态,最后调用 completionHandler 进行刷新后的一些处理。创建状态机需要提供一个遵循 OperatorType 协议的类型,对于刷新来说就是提供一个 RefreshOperator 或它的子类。

  • Controller

要想在 controller 中使用状态机,你需要让它遵循并实现 Refreshable 协议。此协议规定了 refreshableViewrefreshStateMachine 两个属性及 bindRefreshStateMachine()bindRefreshStateMachine(_:) 两个方法。因为在扩展中给出了两个方法的实现,所以要实现此协议只需定义两个属性即可。为方便使用,定义了 TViewControllerCViewControllerSViewController 三个类,分别对应页面是 table viewcollection viewscroll 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

接着是动作,刷新动作只有两种:pullToRefreshloadingMore,对应的过渡状态分别为 refreshingpaging 。由此给出 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 用来定义 targettarget 对应的页面也许能够刷新,也许不能够刷新,我们需要一个布尔属性进行判断,即 isRefreshabletarget 在刷新时会发生变化,所以需要有一个 mutatingupdate(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

让我们来实现自动刷新功能。有了上面的基础,自动刷新其实很容易实现,只需要在 headerfooter 中分别给状态机传入 pullToRefreshloadingMore 动作,并在结束后判断是否显示 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