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