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遵守了TableViewProtocolEmptyProtocolLoadingAnimationProtocol这三个协议,协议里的默认实现就不列出来了都是根据自己项目的需求来展示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
    }()

控制器只需要遵守协议,并实现就可以了,列表的显示由控制器自己处理。就这样就把下来刷新和加载更多,以及网络请求数据错误的展示的逻辑都隐藏起来了,把冗余的代码去掉了。

写在最后,基于老项目的原因牵一发而动全身,这个协议还不是很彻底也不是很纯净,里面的一些东西对于别的项目可能是不需要的,这个提供一个思路大家可以根据自己的实际项目逻辑优化

喜欢就点个赞👍

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 前不久,赵薇在杭州举办的一次关于"女性力量"的采访中,被问到"如何让婚姻保鲜?"她的回答是:"我从来不对婚姻抱任何...
    湯小玉阅读 276评论 0 1
  • 各位优秀的朋友们大家现在好! 请问您有听过演讲吗? 请问您有看过演说家的演讲吗? 请问您有体验过公众演讲吗?其实,...
    8340d620773f阅读 229评论 0 0
  • 发现个有趣的github.user 各种自定义小控件
    秀才不才阅读 282评论 0 1
  • 那一晚回家,你一脸委屈 我搂着肩问你,怎么了 你说,课后有同学拍你肩,转头的瞬间,他用带红外线的手电筒照你眼,正中...
    西泠静阅读 150评论 0 0
  • 20190221猫珍故事汇 (第037) 今天下班,走路20分钟,排队15分钟进地铁站,坐25分钟地铁,与小橙会面...
    遇柒ML阅读 106评论 0 0