Swift Protocal实战1(Refreshable)

Swift Protocal实战1(Refreshable)

在app的开发中,出现最多的一个情况就是显示一个列表来展示数据,就像刷微博一样,要能够上拉加载更多,下拉进行刷新。但在实际开发过程中,需要考虑的情况会更多。我们使用header来表示下拉刷新控件,使用footer来表示上拉加载控件。

在不考虑缓存的情况下单数据源tableView需要考虑以下注意点

  1. 首次进入这个页面,没有数据需要进行首次数据加载
  2. 首次加载过程中不能显示footer
  3. 下拉刷新需要用返回的结果覆盖数据源的数据
  4. 下拉刷新后需要还原footer的状态(变更为可以加载更多)
  5. 上拉加载成功后需要根据返回数据数量来判断是否还有更多数据,没有更多数据需要修改footer的状态为没有更多数据了,并禁止上拉刷新功能。
  6. 首次加载数据如果没有网络连接或者加载失败,需要显示一个失败页面,点击失败页面能够重新进行网络请求获取数据。
  7. 每次进行下拉刷新都需要将当前page设为1
  8. 每次进行上拉加载前都需要将当前page进行+1操作
  9. 每次上拉加载失败都需要将当前page进行-1操作,以还原防止,下次上拉加载page多加了的问题

如果是多数据源的tableView则需要考虑的更多

  1. 首先就是不同数据源的page记录,每个数据源都需要对应一个自己的page
  2. 每个数据源都需要记录是否还有更多数据可供加载
  3. 甚至每个数据源拥有各自的没数据的文字提示
  4. 实际项目中,为了一些效果,还需要记录当前数据源是否处于加载数据状态,以此来显示某些加载页面。
  5. 可能还会有一个个性化的定制需求

综上所述,仅仅是一个控制器的数据加载逻辑就有这么多,如果有很多这样类似的页面,每次都要考虑这么多问题,难免会有不少疏忽,而且要实现这些功能会产生大量的重复代码,这肯定是我们不希望看到的。

分析上述需求我们发现,实际上通常我们所接触的tableView大体上也就需要注意这么多问题,而且为了整个工程的统一性,一般情况所有的处理也是采用同一套逻辑,因此我们完全可以把这些逻辑统一起来,使用一个Protocol来实现这些逻辑。得益于Swift强大的Protocol Extention 大部分情况我们只需要在合适的关键点调用几个方法就可以了,所有的逻辑默认都已经实现了。Controller中的代码更少了,不相关的逻辑都封装好了,逻辑更加简洁了。

控制器使用代码

class PYMyOrderListController: PYBaseViewController, Refreshable {
    internal var refreshStatus: [(page: Int, isLoading: Bool, noMoreData: Bool, noMoreTitle: String)] = [(1,false,false,"没有更多订单了")]     // 定义每个数据源需要的四个属性,分别是当前页码,是否被正在加载中,是否没有更多数据可供加载了。没有数据可供加载的footer文字
    internal var currentIndex = 0       // 当前现实的数据源索引
    internal var refreshTable: UITableView = UITableView()      // 当前tableView
   
    override func viewDidLoad() {
         view.addSubview(tableView)
         tableView.snp_makeConstraints { (make) in
                     make.left.right.bottom.equalTo(0)
                     make.top.equalTo(segementView.snp_bottom)
             }
         refreshTable = tableView       // 赋值当前tableView
         setupRefreshHeader()           //初始化下拉刷新控件
         setupRefreshFooter()           // 初始化上拉加载控件
     }    
        ///  加载数据的方法
        ///
        ///  - parameter isRefresh: 是否是下拉刷新
    func refreshData(isRefresh: Bool) {    
        refreshStatus[currentIndex].isLoading = true        // 修改当前数据源的加载状态为正在加载
        let indexItem = currentIndex
        let url = URL_OrderList + "/\(type)/\(PageCount)" + "/\(refreshStatus[currentIndex].page).json"
        let request = PYNetWorkTools.GET(url, hudType: .None, failer: { [weak self] (failerTuples) in
            guard self != nil else{ return }
            self?.loadFailer(failerTuples)      // 加载失败的方法
        }) {[weak self] (response, jsonResult) in
            guard self != nil else{ return }
            self?.tableView.hiddenNoNetPlace()
            let array = PYOrderListModel.modelArray(jsonResult)
            if isRefresh {
                self?.totalArray[indexItem] = array
            } else {
                self?.totalArray[indexItem] += array
            }
            self?.loadSuccess(array.count < PageCount)      // 加载成功的方法,并传递一个是否还有更多数据的返回值
        }
        if request != nil {
            requests.append(request!)
        }
    }
}    

以上这些代码就可以实现上述所有的功能,怎么样,是不是很有魅力呢?实例中使用了Refreshable协议,这套协议可以用在UIViewControllerUITableViewController中,其中的refreshTable就是为了适配UIViewController所增加的一个属性,否则连这个属性都不用写了。

代码中能看到的协议中定义的内容如下

  • 属性

    • refreshStatus
    • currentInidex
    • refreshTable
  • 方法

    • refreshData(isRefresh: Bool)
    • setupRefreshHeader()
    • setupRefreshFooter()
    • loadFailer(failerTuples: FailerTuples)
    • loadSuccess(noMoreData: Bool?)

让我们先看看Refreshable这个协议里是怎么写的

///  刷新协议
protocol Refreshable {
    ///  刷新数据的方法,必须实现,调用这个方法来执行下拉刷新和上拉加载
    ///
    ///  - parameter isRefresh: 是否是下拉刷新
    func refreshData(isRefresh: Bool)
    
    /// 刷新状态的四个参数,分别是,当前页码,是否正在加载中,是否没有更多数据了,没有更多数据的footer显示文字
    var refreshStatus: [(page: Int, isLoading: Bool, noMoreData: Bool, noMoreTitle: String)] {set get}
    
    /// 当前数据源的索引号
    var currentIndex: Int {set get}
    
    /// 需要处理的tableView
    var refreshTable: UITableView {get set}
}

// MARK: - 遵守这个协议的是控制器
extension Refreshable where Self: UIViewController {

    ///  加载数据失败调用此方法
    ///
    ///  - parameter failerTuples: 失败原因
    mutating func loadFailer(failerTuples: (type: NetFailerType, desc: String?)?) {
        refreshStatus[currentIndex].page -= 1
        if refreshStatus[currentIndex].page < 0 {
            refreshStatus[currentIndex].page = 0
        }
        refreshStatus[currentIndex].isLoading = false
        refreshFooter()
        if refreshTable.mj_header != nil {
            refreshTable.mj_header.endRefreshing()
        }
        
        if failerTuples?.type == NetFailerType.NoNet {
            if refreshTable.visibleCells.isEmpty {
                refreshTable.showNoNetPlace({ [weak self] in
                    guard self != nil else { return }
                    self?.refreshData(true)
                    })
            } else {
                showToast(failerTuples?.type.rawValue ?? "")
            }
        }
        refreshTable.reloadData()
    }
    
    ///  加载数据成功调用此方法
    ///
    ///  - parameter noMoreData: 是否没有更多数据了
    mutating func loadSuccess(noMoreData: Bool?) {
        refreshStatus[currentIndex].isLoading = false
        if let noMoreData = noMoreData where refreshTable.mj_footer != nil {
            refreshStatus[currentIndex].noMoreData = noMoreData
            refreshTable.mj_footer.hidden = false
            refreshFooter()
        }
        if refreshTable.mj_header != nil {
            refreshTable.mj_header.endRefreshing()
        }
        refreshTable.reloadData()
    }
    
    ///  初始化下拉刷新控件
    func setupRefreshHeader() {
        let header = MJRefreshNormalHeader {[weak self] () -> Void in
            guard self != nil else { return }
            self?.refreshTable.mj_footer.resetNoMoreData()
            self?.refreshStatus[self!.currentIndex].page = 1
            self?.refreshData(true)
            self?.refreshStatus[self!.currentIndex].isLoading = true
        }
        refreshTable.mj_header = header
    }
    
    ///  初始化上拉加载控件
    func setupRefreshFooter() {
        let footer = MJRefreshBackStateFooter {[weak self] () -> Void in
            guard self != nil else { return }
            self?.refreshStatus[self!.currentIndex].page += 1
            self?.refreshData(false)
            self?.refreshStatus[self!.currentIndex].isLoading = true
        }
        footer.hidden = true
        refreshTable.mj_footer = footer
    }
    
    ///  刷新上拉加载控件,用来重置上拉刷新控件状态,控制能够刷新以及显示类型
    func refreshFooter() {
        if refreshTable.mj_footer != nil {
            if refreshStatus[currentIndex].noMoreData {
                refreshTable.mj_footer.endRefreshingWithNoMoreData()
            } else {
                refreshTable.mj_footer.endRefreshing()
            }
        }
    }
}

整个协议简洁明了,没有一句废话。就把众多需要的功能及注意点都涵盖了。该协议具有以下特点。

  • 支持UITableViewController以及UIViewcontroller的刷新处理。
  • 支持多数据源的切换加载。

这两个特性已经涵盖了日常开发中常见的所有情况。当然你也可以只添加上拉加载,不添加下拉刷新功能,总之,这些都随便。

源码在这

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,970评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,080评论 4 62
  • 说起陈继儒,大家也许不怎么熟悉,他是明代知名文学家、书画家,字仲醇,号眉公、麋公。有《梅花册》、《云山卷》、《陈眉...
    娑婆如斯阅读 498评论 10 33