软件环境:Xcode 13.2、swift 5
创建时间:2022年 03月10号
适用范围:iOS项目
这篇文章都说了什么
- 在iOS中,如何优化tableView,提升效率(高度计算、事件回调),降低代码量
- 源码分享(swift+OC),快速在你的项目中搭建
背景
tableView是我们比较常用的容器类组件,每个页面,无论是否超过一屏,使用scrollView的弹性效果,能更贴合iPhone温柔的用户体验。所以,app的大部分页面几乎都要用到tableView。
假如有下面这样一个页面,其中某组数据在一些场景中可能有也可能没有,如果用tableView原生来实现,两个主要的代理函数的代码写起来会很复杂,判断每个indexPath,设置不同cell。
结合table复用的特点,table的内容展示、cell高度、section头等信息如果每次滚动都需要计算,就会耗费一定的性能,而且页面复杂的时候,代码量很大。所以如果我们能一次性计算完成,然后让table的代理直接调用计算后的结果,会好一些。本文就以减少频繁计算、降低编码难度为出发点,来对tableView做一层包装。
以下,以swift代码为例
一、主要思路
如果不想每次滚动都计算高度和查找cell,那就把高度和cell与indexPath的关系通过一个管理类管理起来,比如有多少个section,每个section包含哪些cell,每个cell的高度是多少、依赖哪些数据、点击事件回调等都设置好
二、主要类及其功能
1、概要
我们结合表格来看一下这个管理工具各个类的作用
类名 | 作用 |
---|---|
HTableViewManager | 管理主类,实现tableView的代理,用来把下面的模型类转翻译并传给tableView的代理函数,主要对tableView的基本属性(布局样式等)及section进行管理 |
HTableViewSectionModel | 匹配tableView的section,主要包含对tableView的row进行管理 |
HTableViewRowModel | 匹配tableView的row,主要包含对row的高度、数据、交互事件的管理 |
HTableViewSectionHeaderModel | 匹配tableView的“viewForHeaderInSection”,控制每组的头 |
HTableViewRowEditingModel | 匹配tableView的editingStyleForRowAt IndexPath,控制每行的编辑样式 |
HBaseTableViewCell | UITableViewCell的子类,从HTableViewManager中获取数据、交互、回调等 |
HBaseSeparateTableViewCell | HBaseTableViewCell的子类,简易分割线,把一个cell当成分割线,减少代码量,部分场景适用,可选。 |
我们只要把每一块的数据都按照展示顺序以HTableViewRowModel的实例装入HTableViewSectionModel里面,然后把页面依赖的tableViewCell画出来,然后交给HTableViewManager去管理和展示,而无需关注具体管理细节。
结合下图,我们立体的看一下各个类之间的关系
结合UI展示,看看整体结构关系
如果看了这些,你还不清楚整体逻辑关系,就保留一些疑问,卡看具体代码吧
2、具体介绍
HTableViewManager,实现tableView的代理函数,管理sections,从sections里面获取到一共有多少个分组,展示每个分组的row及其编辑效果、展示每个分组的header
import UIKit
/// 代理
@objc public protocol HTableViewManagerDelegate : NSObjectProtocol {
@objc optional func tableViewDidScroll(_ tableView: UITableView)
}
/// 数据
@objc public protocol HTableViewManagerDataSource : NSObjectProtocol {
@objc optional func tableViewDataSource(_ tableManager: HTableViewManager)
}
open class HTableViewManager: NSObject, UITableViewDataSource, UITableViewDelegate {
weak var delegate :HTableViewManagerDelegate?
weak var dataSource :HTableViewManagerDataSource?
var tableView: UITableView?
/// sections with rows
lazy var sections: NSMutableArray = {
let sections = NSMutableArray.init()
return sections
}()
public init(tableView: UITableView, dataSource: HTableViewManagerDataSource) {
super.init()
self.tableView = tableView
self.tableView?.delegate = self
self.tableView?.dataSource = self
self.dataSource = dataSource
self.tableView?.separatorStyle = .none
self.tableView?.showsVerticalScrollIndicator = false
if #available(iOS 11.0, *) {
self.tableView?.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 15.0, *) {
self.tableView?.sectionHeaderHeight = 0
}
}
// MARK: func
/// 添加一个 section
func addSection(_ section: HTableViewSectionModel) {
sections.add(section)
}
/// 指定位置插入section
func insertSection(_ section: HTableViewSectionModel, at index: NSInteger) {
sections.insert(section, at: index)
}
/// 清空所有
func removeAllSection() {
sections.removeAllObjects()
}
/// 直接添加row(添加到最后一个section,如果没有section会创建一个section)
func addRow(_ row: HTableViewRowModel) {
var lastSection: HTableViewSectionModel
if sections.count > 0 {
lastSection = sections.lastObject as! HTableViewSectionModel
} else {
lastSection = HTableViewSectionModel()
sections.add(lastSection)
}
lastSection.addRow(row)
}
/// 批量添加row,添加到最后一个section
func addRows(_ rows: [HTableViewRowModel]) {
var lastSection: HTableViewSectionModel
if sections.count > 0 {
lastSection = sections.lastObject as! HTableViewSectionModel
} else {
lastSection = HTableViewSectionModel()
sections.add(lastSection)
}
lastSection.rows.addObjects(from: rows)
}
/// 刷新(相当于执行tableView.reloadData(),此时会调用tableViewDataSource重新获取数据)
func reloadTableView() {
sections.removeAllObjects()
dataSource?.tableViewDataSource?(self)
tableView?.reloadData()
}
// MARK: table data source
public func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionModel :HTableViewSectionModel = sections[section] as! HTableViewSectionModel
return sectionModel.rows.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section :HTableViewSectionModel = sections[indexPath.section] as! HTableViewSectionModel
let row :HTableViewRowModel = section.rows[indexPath.row] as! HTableViewRowModel
//cell
var cell = tableView.dequeueReusableCell(withIdentifier: row.reuserIdentifier)
if cell == nil {
let newCell :HBaseTableViewCell.Type = row.cellClasss as! HBaseTableViewCell.Type
cell = newCell.init(style: UITableViewCell.CellStyle.default, reuseIdentifier: row.reuserIdentifier)
}
(cell as! HBaseTableViewCell).row = row
(cell as! HBaseTableViewCell).cellWillAppear()
return cell!
}
// MARK: delegate ------------------
// MARK: basic
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let section :HTableViewSectionModel = sections[indexPath.section] as! HTableViewSectionModel
let row :HTableViewRowModel = section.rows[indexPath.row] as! HTableViewRowModel
return CGFloat(row.rowHeight)
}
/// 支持 section header
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let sectionModel :HTableViewSectionModel = sections[section] as! HTableViewSectionModel
if sectionModel.headerView == nil {
return nil
} else {
return sectionModel.headerView?.headerView
}
}
public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let sectionModel :HTableViewSectionModel = sections[section] as! HTableViewSectionModel
if sectionModel.headerView == nil {
return 0
} else {
return CGFloat((sectionModel.headerView?.headerHeight)!)
}
}
/// 支持编辑
public func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
let section :HTableViewSectionModel = sections[indexPath.section] as! HTableViewSectionModel
let row :HTableViewRowModel = section.rows[indexPath.row] as! HTableViewRowModel
if row.editingStyles.count == 0 {
return .none
} else {
return .delete
}
}
public func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let section :HTableViewSectionModel = sections[indexPath.section] as! HTableViewSectionModel
let row :HTableViewRowModel = section.rows[indexPath.row] as! HTableViewRowModel
if row.editingStyles.count == 0 {
return []
} else {
var actions :[UITableViewRowAction] = []
var index = 0
for modelx in row.editingStyles {
let model = (modelx as! HTableViewRowEditingModel)
let action = UITableViewRowAction.init(style: .default, title: model.title) { (action, indexPath) in
row.didEditRowWithActionAtIndex(index)
}
action.backgroundColor = model.backgroundColor
actions.append(action)
index += 1
}
return actions
}
}
/// 代理滚动
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
delegate?.tableViewDidScroll?(tableView!)
}
// 其他代理,按需补充
// 按需刷新,感兴趣自己试试
HTableViewSectionModel,管理每个分组的rows
/// sectiion data model
open class HTableViewSectionModel: NSObject {
/// 添加一行
public func addRow(_ row: HTableViewRowModel) {
rows.add(row);
}
/// 添加分割线
public func addSeparateRowWithHeight(height: Double, color: UIColor) {
let row = HTableViewRowModel.separateRow(height: height, color: color);
addRow(row)
}
/// 插入一行
public func insertRow(row: HTableViewRowModel, atIndex: Int) {
rows.insert(row, at: atIndex)
}
/// 删除一行
public func deleteRowAtIndex(_ index: Int) {
rows.removeObject(at: index)
}
/// 所有行
lazy var rows: NSMutableArray = {
let rows = NSMutableArray.init()
return rows
}()
/// section header
var headerView :HTableViewSectionHeaderModel?
}
HTableViewRowModel,包含每个row用哪个UITableViewCell来展示、行高、数据(多种类型可选)、事件回调(多种方式可选)
import UIKit
/// row data model
open class HTableViewRowModel: NSObject {
override public init() {
super.init()
model = "" as AnyObject
}
static func row() ->HTableViewRowModel {
return HTableViewRowModel.init()
}
/// 行高
var rowHeight = 44.0
/// cellClasss
var cellClasss: AnyClass = HBaseTableViewCell.self
/// 模型
public var model : AnyObject?
/// 备用模型
public var subModel : AnyObject?
/// 字符串model
public var strModel : String?
/// 整型model
public var intModel : Int?
/// 布尔model
public var boolModel : Bool?
/// 圆角模型
public var borderModel : HTableViewBorderModel?
/// 复用标识,特殊情况同一个cell分开复用 (默认 = "cellName" )
private var _reuserIdentifier = ""
public var reuserIdentifier: String {
get {
if (_reuserIdentifier.count > 0) {
return _reuserIdentifier
}
else {
return NSStringFromClass(cellClasss)
}
}
set {
_reuserIdentifier = newValue
}
}
// MARK: 回调接口
/// 回调
public var didSelectRow : () -> () = {}
/// 回调index
public var didSelectRowAtIndex : (_ index: Int) -> () = {_ in}
/// 回调index
public var ifCanSelectRowAtIndex : (_ index: Int) -> (Bool) = {_ in return true}
/// 回调Any
public var didCallBackWithData : (_ data: AnyObject) -> () = {_ in}
/// 回调Any + index
public var didCallBackwithDataAndIndex : (_ data: AnyObject, _ index: Int) -> () = {data, index in}
/// 编辑
public var didEditRowWithActionAtIndex : (_ index: Int) -> () = {_ in}
/// 类方法,便利构造器
class func separateRow(height: Double, color: UIColor, leftMargin: Double = 0.0, righMargin: Double = 0.0) -> HTableViewRowModel {
let row = HTableViewRowModel.init();
row.rowHeight = height
row.cellClasss = HBaseSeparateTableViewCell.self
row.model = ["\(leftMargin)", "\(righMargin)"] as AnyObject
row.subModel = color
return row
}
/// 编辑模式
private var _editingStyles: NSMutableArray = {
let styles = NSMutableArray.init()
return styles
}()
var editingStyles : NSMutableArray {
get{
return _editingStyles
}
}
public func addEditStyle(_ style: HTableViewRowEditingModel) {
_editingStyles.add(style)
}
}
HTableViewSectionHeaderModel和HTableViewRowEditingModel,主要表达每组的头样式,每个cell的编辑样式
/// section header data model
open class HTableViewSectionHeaderModel {
var headerHeight = 0.0
var headerView :UIView?
}
/// row edit model
open class HTableViewRowEditingModel {
var backgroundColor = UIColor.red
var title = "编辑"
}
HBaseTableViewCell,UITableViewCell的子类,主要接收和使用上面模型类的数据、回调方式
import UIKit
open class HBaseTableViewCell: UITableViewCell {
public var row: HTableViewRowModel?
required public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
self.contentView.addSubview(highLightView)
highLightView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
/// 后面使用用的时候,直接在这里初始化cell即可
self.cellDidLoad()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
/// called when init
public func cellDidLoad(){
}
/// called when table view reload data or cell will appear
func cellWillAppear() {
}
// MARK: touche and high light
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
highLightView.backgroundColor = kCOLOR_BGF2
}
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
highLightView.backgroundColor = .clear
self.row?.didSelectRow()
}
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
highLightView.backgroundColor = .clear
}
lazy var highLightView: UIView = {
let view = UIView.init()
return view
}()
// MARK: quick get the model
/// Height of the cell.
public var rowHeight :Double {
get {
return row?.rowHeight ?? 0
}
}
/// Main model.
public var model :AnyObject {
get {
return (row?.model)!
}
}
/// Sub model if the vc need more objc to control the cell.
public var subModel :AnyObject {
get {
return (row?.subModel)!
}
}
/// String model.
public var strModel :String {
get {
return (row?.strModel) ?? ""
}
}
/// Int model.
public var intModel :Int {
get {
return (row?.intModel) ?? 0
}
}
/// Bool model.
public var boolModel :Bool {
get {
return (row?.boolModel) ?? false
}
}
}
3、使用示例
1)初始化,建议自己封装一个tableView的控制器,以后初始化的代码省略了
class SWBaseTableViewController: SWBaseViewController, HTableViewManagerDataSource {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.addSubview(tableView)
tableView.snp.makeConstraints({ (make) in
make.edges.equalToSuperview()
});
}
lazy var tableView: UITableView = {
let tableView = UITableView.init()
var topInset = CGFloat(kNaviHeight)
if is_fullScreen == false {
topInset = 54
}
tableView.contentInset = UIEdgeInsets.init(top: topInset, left: CGFloat(0.0), bottom: CGFloat(kTabHeight), right: CGFloat(0.0))
return tableView
}()
lazy var tableManager: HTableViewManager = {
let tableManager = HTableViewManager.init(tableView: tableView, dataSource: self)
return tableManager
}()
}
2)展示不同的内容
// 代理函数
func tableViewDataSource(_ tableManager: HTableViewManager) {
// section1
tableManager.addSection(itemSection())
// section2、3、4,按需区分不同的卡片
}
// section
func itemSection() ->HTableViewSectionModel {
let section = HTableViewSectionModel.init()
let row = HTableViewRowModel.init()
// 实际的cell
row.cellClasss = HUserTableViewCell.self
// cell 要展示的数据
let dataList = [1,2,3] // 这里使用你实际要展示的数据,可以传各种模型
row.model = dataList as AnyObject
// cell 高度设定
row.rowHeight = dateList.size * 20
section.addRow(row)
// 可以再加其他cell
// section.addRow(row1)
// section.addRow(row2)
return section
}
3) 事件回调
row.didSelectRow = {[weak self] in
// 点击整个cell
}
row.didSelectRowAtIndex = {[weak self] index in
// 点击cell的不同元素,根据index处理不同的点击/回调
}
row.didCallBackWithData = {[weak self] data in
// 从cell中回调数据给控制器
}
4)sectionHeader
/// section header
var headerView :HTableViewSectionHeaderModel?
5)cell编辑
let editModel = HTableViewRowEditingModel()
editModel.title = "编辑"
editModel.backgroundColor = .red
row.addEditStyle()
row.didEditRowWithActionAtIndex = { index in
// 处理编辑事件
}
以上就是封装后的使用示例,可以发看到,卡片化管理使得代码少了很多、模型保存了tableView代理所需要的所有数据。
三、总结
优化的思路很明显————模型控制。大致理解思路后可结合你的业务特点自己封装,优化掉其中不适合你的细节点,在实际的业务中不断打磨,慢慢地变成自己比较熟悉的一个组件。每个公司的产品和UI设计区别都很大,他们的风格直接影响你各个组件的能力。
在这个框架之下,我已经进行约6个app的开发和维护,都是经过打磨和验证的。最后,附上OC的实现方式