美文网首页聊天功能设计
聊天功能(五)--ChatTableView的刷新能力Reloa

聊天功能(五)--ChatTableView的刷新能力Reloa

作者: Mage | 来源:发表于2018-06-07 17:48 被阅读16次

    一、Reloadable定义

    在处理聊天功能时,有可能会出现以下几种场景:
    1、添加消息,需要使用UITableView.insertRows(at:with:);
    2、撤回消息,需要显示内容“该消息已被对方撤回”,需要使用UITableView.reloadRows(at:with:);
    3、删除消息,需要使用UITableView.deleteRows(at:with:)。

    创建Reloadable协议,使UITableView同时支持添加消息、刷新消息、删除消息的能力。首先先将协议定义好。支持协议的类需要提供models和刷新使用的tableView,并且model必须支持Comparable。之后直接调用refresh()方法即可更新数据。

    // MARK: - UITableView扩展
    public protocol Reloadable {
    
        associatedtype Model: Comparable, Hashable
        
        var models: [Model] { get set }
        var reloadTableView: UITableView { get }
        
        mutating func refresh(_ models: [(Model, RefreshMode)])
    }
    extension Reloadable where Model: Comparable {
        
        public mutating func refresh(_ models: [(Model, RefreshMode)]){
        }
    }
    

    二、RefreshMode定义

    RefreshMode用于定义消息是插入还是删除(更新消息不能确定这个消息是否已经存在,所以将其并入insert里,当存在该消息时,则就是更新消息)。同时还需要指定该消息插入或删除后,是否将tableView滚动到底部。

    public enum ScrollType {
        case none   // 不滚动
        case bottom // 滚动到底部
        case hold   // 滚动到原来的位置,界面上不动
    }
    public enum RefreshMode {
        case insert(ScrollType) // 插入数据
        case delete(ScrollType) // 删除数据
        
        public var type: ScrollType {
            switch self {
            case .insert(let type):
                return type
            case .delete(let type):
                return type
            }
        }
        
        public var isDelete: Bool {
            switch self {
            case .delete(_):
                return true
            default:
                return false
            }
        }
        
        public var isInsert: Bool {
            switch self {
            case .insert(_):
                return true
            default:
                return false
            }
        }
    }
    

    3、refresh方法

    refresh方法接收[(Model, RefreshMode)]数组,里面包含models和model的刷新策略。

    1、需要将models根据刷新策略分类成deleteModels、insertModels和reloadModels。
    2、self.models删除掉deleteModels,然后添加insertModels。
    3、计算对应的indexPath数组。deleteIndexPaths和reloadIndexPaths需要根据oldModels对应的位置创建indexPath,insertIndexPaths需要根据更新后的self.models对应的位置创建indexPath。
    4、判断滚动策略:
    4.1、如果scrollType == .bottom,则直接使用scrollToRow滚动到tableView的底部
    4.2、如果scrollType == .hold,由于如果models中存在删除或插入的数据,tableView上的可见的cell会向上或向下移动,体验不好,所以需要计算tableView可见的最后一个cell的位置的偏移量,当刷新完row时重新设置偏移量,达到界面保持不动的效果。
    5、更新Rows,刷新操作需要放在beginUpdates()和endUpdates()

    extension Reloadable where Model: Comparable {
        
        public mutating func refresh(_ models: [(Model, RefreshMode)]){
            
            if models.count == 0 { return }
            let oldModels = self.models
            
            var addModels: Set<Model> = Set()
            var deleteModels: Set<Model> = Set()
            
            var scrollType: ScrollType = .none
            models.forEach { (model, type) in
                scrollType = type.type
                if type.isInsert {
                    deleteModels.remove(model)
                    addModels.insert(model)
                }else{
                    addModels.remove(model)
                    deleteModels.insert(model)
                }
            }
            // 处理数据
            self.models.remove(contentsOf: deleteModels)
            
            let orders = self.models.insertOrder(contentsOf: addModels)
            
            let deleteIndexPaths: [IndexPath] = deleteModels.compactMap{ (model) -> IndexPath? in
                if let index = oldModels.index(of: model) {
                    return IndexPath.init(row: index, section: 0)
                }else{ return nil }
            }
            let reloadIndexPaths: [IndexPath] = orders.1.compactMap { (model) -> IndexPath? in
                if let index = oldModels.index(of: model) {
                    return IndexPath.init(row: index, section: 0)
                }else{ return nil }
            }
            let insertIndexPaths: [IndexPath] = orders.0.compactMap { (model) -> IndexPath? in
                if let index = self.models.index(of: model) {
                    return IndexPath.init(row: index, section: 0)
                }else{ return nil }
            }
            
            var bottomIndexPath: IndexPath? = nil
            // bottom的偏移量
            var offsetInset: CGFloat? = nil
            if let cell = self.reloadTableView.visibleCells.last, let indexPath = self.reloadTableView.indexPath(for: cell), indexPath.row < oldModels.count {
                let model = oldModels[indexPath.row]
                if let index = self.models.index(of: model) {
                    bottomIndexPath = IndexPath.init(row: index, section: 0)
                    offsetInset = cell.frame.maxY - self.reloadTableView.frame.height - self.reloadTableView.contentOffset.y
                }
            }
            UIView.noAnimation {
                self.reloadTableView.beginUpdates()
                if deleteIndexPaths.count > 0{
                    self.reloadTableView.deleteRows(at: deleteIndexPaths, with: .none)
                }
                if insertIndexPaths.count > 0{
                    self.reloadTableView.insertRows(at: insertIndexPaths, with: .none)
                }
                if reloadIndexPaths.count > 0{
                    self.reloadTableView.reloadRows(at: reloadIndexPaths, with: .none)
                }
                self.reloadTableView.endUpdates()
                
                if scrollType == .bottom {
                    self.reloadTableView.scrollToRow(at: IndexPath.init(row: self.models.count-1, section: 0), at: .bottom, animated: true)
                }else if scrollType == .hold && bottomIndexPath != nil && offsetInset != nil{
                    self.reloadTableView.scrollToRow(at: bottomIndexPath!, at: .none, animated: false)
                    var offset = self.reloadTableView.contentOffset
                    offset.y -= offsetInset!
                    self.reloadTableView.contentOffset = offset
                }
            }
        }
    }
    

    上一篇:聊天功能(四)--创建ChatViewController

    Demo地址:MAChatTableViewDemo

    相关文章

      网友评论

        本文标题:聊天功能(五)--ChatTableView的刷新能力Reloa

        本文链接:https://www.haomeiwen.com/subject/jwxgsftx.html