Swift利用Protocol封装UITableView下拉刷新
前言:在公司iOS端项目中一直有一个比较棘手的问题就是列表的问题,列表控制器包括列表的展示、下拉刷新、上拉加载更多数据、以及错误界面的处理。逻辑复杂,以前的代码冗余度也比较高,所以抽出时间对列表做了基于协议的封装RefreshTableViewProtocol
。
RefreshTableViewProtocol
前必须要介绍ListResponseProtocol
协议
protocol ListResponseProtocol:HandyJSON{
associatedtype T
var slide: Int {get set}
var top: String {get set}
var bottom: String {get set}
var hasMore: Bool {get set}
var list: [T] {get set}
mutating func add<List:ListResponseProtocol>(data:List) where List.T == T
}
extension ListResponseProtocol{
mutating func add<List:ListResponseProtocol>(data:List) where List.T == T {
slide = data.slide
top = data.top
bottom = data.bottom
hasMore = data.hasMore
list.append(contentsOf: data.list)
}
}
ListResponseProtocol
是定义的我们服务器返回的列表数据的整体结构,定义这个主要是加载更多的列表需要固定的数据结构, 服务器返回的数据结构是稳定的,当然这也有一定的灵活性,只要遵循了这个协议的数据都可以被列表协议使用
比如QueryListResponse
由于项目的原因需要特殊的字段都可以,提高灵活性
class QueryListResponse<T>: HandyJSON,ListResponseProtocol {
var slide = 0
var top = ""
var bottom = ""
var hasMore = false
var list: [T] = []
//这个是新加的字段来接受服务器的数据
var user:QueryUser?
required init() {}
}
接下来看看RefreshTableViewProtocol的代码
(这里的代码还是可以优化的,只是最近有点忙,一直没做)
protocol RefreshTableViewProtocol:TableViewProtocol,EmptyProtocol,LoadingAnimationProtocol{
associatedtype T:ListResponseProtocol
var http:ModelHTTP<T> { get set }
var resp: T {get set}
func loadDataSuccess(_ model: T,_ isLoadAnimation:Bool,_ append:Bool)
func processData(_ model: T, append: Bool)
func loadDataFailed(_ code: Int?, _ msg:String,_ isLoadAnimation:Bool,_ append:Bool)
//在请求数据的前要做的其它事情
func beforeLoadDataAction(_ isLoadMore:Bool,_ isLoadAnimation:Bool)
//在处理数据的时候要做的其它事情
func processDataOtherAction(_ model: T, _ append: Bool)
//处理请求错误
func loadingFailure(_ code: Int?, _ msg:String,_ isLoadAnimation:Bool)
}
RefreshTableViewProtocol
遵守了TableViewProtocol
、EmptyProtocol
、LoadingAnimationProtocol
这三个协议,协议里的默认实现就不列出来了都是根据自己项目的需求来展示UI
TableViewProtocol
主要提供Tableview创建的一些便利方法
protocol TableViewProtocol:class {
var dataTableView:UITableView {get set}
var header:MJRefreshHeader { get set }
var footer:MJRefreshFooter { get set }
var footerView:NoMoreFooter { get set }
func headerRefreshAction()
func footerRefreshAction()
}
EmptyProtocol
主要提供的是空白页面的处理
protocol EmptyProtocol:EmptyViewDelegate {
func showEmptyWith(type: EmptyType, on superView: UIView, withText text: String? , isFullScreen: Bool, offSetY: CGFloat?, showRefreshBtn: Bool)
func hideEmpty(on superView: UIView)
}
LoadingAnimationProtocol
主要是提供加载的动画
protocol LoadingAnimationProtocol:UIViewController {
func showLoading(offsetX: CGFloat,offsetY: CGFloat, withShadowBackground: Bool)
func hideLoading(withShadowBackground: Bool)
func hideLoadingWithTime(time: TimeInterval)
}
RefreshTableViewProtocol
遵守了上面的三个协议就有了三个协议提供的能力
associatedtype T:ListResponseProtocol
这个定义的协议的泛型,就是在遵守这个协议的时候必须指定的一个数据类型,并且这个数据类型必须遵守ListResponseProtocol
这个协议
var http:ModelHTTP<T> { get set }
这个是协议必须提供一个ModelHTTP<T>
的数据,这个其实是发起网络请求,这个网络请求是前期封装的一个网络请求工具,在这里刚好派上用场
var resp: T {get set}
主要是对网络请求的数据做一个缓存
其它的方法都是为了使用的灵活性而暴露的,可以根据使用时自行定义
接下来看下协议的默认实现
extension RefreshTableViewProtocol{
//下拉刷新
func headerRefreshAction() {
loadDatas()
}
//上拉加载更多
func footerRefreshAction() {
loadMoreDatas()
}
//加载数据有加载动画
func loadDataWithAnimation() {
baseLoadData(isLoadMore: false, isLoadAnimation: true)
}
//加载数据无加载动画
func loadDatas(){
baseLoadData(isLoadMore: false, isLoadAnimation: false)
}
//加载更多数据
func loadMoreDatas() {
baseLoadData(isLoadMore: true, isLoadAnimation: false)
}
//错误和空白界面的刷新按钮点击
func emptyViewDidTapRefresh(_ emptyView: EmptyView){
loadDataWithAnimation()
}
//处理数据
func processDataOtherAction(_ model: T, _ append: Bool){
if model.list.isEmpty {
showEmptyWith(type: .noData, on: dataTableView)
}else{
hideEmpty(on: dataTableView)
}
}
func beforeLoadDataAction(_ isLoadMore:Bool,_ isLoadAnimation:Bool){}
}
extension RefreshTableViewProtocol{
//加载数据
func baseLoadData(isLoadMore:Bool = false,isLoadAnimation:Bool = false){
beforeLoadDataAction(isLoadMore,isLoadAnimation)
if isLoadAnimation{
showLoading()
}
if isLoadMore{
http.parameters?.appendListParameters(resp: resp)
}else{
http.parameters?["top"] = ""
http.parameters?["bottom"] = ""
http.parameters?["slide"] = ""
}
hideEmpty(on: dataTableView)
http.successOnlyRespCallback = { [weak self] model in
guard let strongSelf = self else { return }
strongSelf.loadDataSuccess(model, isLoadAnimation, isLoadMore)
}
http.failedCallback = { [weak self] code,msg in
guard let strongSelf = self else { return }
strongSelf.loadDataFailed(code, msg, isLoadAnimation, isLoadMore)
}
http.doHTTP()
}
//成功
func loadDataSuccess(_ model: T,_ isLoadAnimation:Bool,_ append:Bool){
hideLoading()
endRefresh()
processData(model, append: append)
}
//失败
func loadDataFailed(_ code: Int?, _ msg:String,_ isLoadAnimation:Bool,_ append:Bool){
hideLoading()
endRefresh()
loadingFailure(code,msg,isLoadAnimation)
}
func endRefresh(){
if let header = dataTableView.mj_header{
header.endRefreshing()
}
if let footer = dataTableView.mj_footer{
footer.endRefreshing()
}
}
//处理数据
func processData(_ model: T, append: Bool){
if append {
resp.add(data:model)
}else{
resp = model
}
relfreshMJFooter(tableView:dataTableView, resp: model)
processDataOtherAction(model,append)
dataTableView.reloadData()
}
//处理请求错误
func loadingFailure(_ code: Int?, _ msg:String,_ isLoadAnimation:Bool){
if isLoadAnimation{
dataTableView.mj_header = nil
guard let code = code else { return }
if code == netFailDefaultCode {
showEmptyWith(type: .netFail, on: dataTableView)
} else {
showEmptyWith(type: .error, on: dataTableView)
}
resp = T()
}else{
toast(msg)
}
}
//处理footer
func relfreshMJFooter<T:ListResponseProtocol>(tableView: UITableView, resp: T){
if resp.hasMore {
tableView.mj_footer = footer
tableView.tableFooterView = nil
return
}
if !resp.list.isEmpty {
tableView.mj_footer = nil
tableView.tableFooterView = footerView
return
}
tableView.mj_footer = nil
tableView.tableFooterView = nil
}
}
这里面封装了默认的使用逻辑,处理了网络请求和数据的处理,对下拉刷新,和上拉加载更多,对空白界面都封装了默认的实现。
实际的使用体验
class BalanceListViewController: UIViewController,RefreshTableViewProtocol {
var type:BalanceItemType = .All
//网络请求
lazy var http: ModelHTTP<BalanceListResponse<Balance>> = {
var parameters:Dictionary<String,Any> = ["type":type.rawValue]
return ModelHTTP<BalanceListResponse<Balance>>(method: .get,parameters:parameters,api:.payRecords)
}()
//请求的列表数据
var resp: BalanceListResponse<Balance> = BalanceListResponse<Balance>()
//列表数据类型
typealias T = BalanceListResponse<Balance>
var isLoading : Bool = false
lazy var dataTableView: UITableView = {
let _dataTableView = UITableView(frame: .zero, style: .plain)
_dataTableView.separatorStyle = .none
_dataTableView.backgroundColor = Color_F2F6F8
_dataTableView.showsVerticalScrollIndicator = false
_dataTableView.mj_header = header
_dataTableView.dataSource = self
_dataTableView.delegate = self
_dataTableView.rowHeight = 70.0
_dataTableView.ut_registerNibCell(BalanceListCell.self)
return _dataTableView
}()
//对header进行自定义设置
lazy var header: MJRefreshHeader = {
let header = creatMJHeader() as? RefreshHeader
header?.backgroundColor = Color_F2F6F8
header?.grayLayer.backgroundColor = Color_F2F6F8.cgColor
return header ?? creatMJHeader()
}()
//对footer进行自定义设置
lazy var footer: MJRefreshFooter = {
let footer = creatMJFooter()
footer.backgroundColor = Color_F2F6F8
return footer
}()
//对footerView进行自定义设置
lazy var footerView: NoMoreFooter = {
let footerView = creatFooterView()
footerView.backgroundColor = Color_F2F6F8
return footerView
}()
控制器只需要遵守协议,并实现就可以了,列表的显示由控制器自己处理。就这样就把下来刷新和加载更多,以及网络请求数据错误的展示的逻辑都隐藏起来了,把冗余的代码去掉了。
写在最后,基于老项目的原因牵一发而动全身,这个协议还不是很彻底也不是很纯净,里面的一些东西对于别的项目可能是不需要的,这个提供一个思路大家可以根据自己的实际项目逻辑优化
喜欢就点个赞👍