聊天功能设计

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

2018-06-07  本文已影响16人  Mage

一、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

上一篇下一篇

猜你喜欢

热点阅读