优雅的开发TableView

article/2025/8/26 11:24:04

前言

UITableView(UITableViewController)是iOS开发使用频率最高的一个组件。

不管是使用UITableView还是还是UITableViewController,在开发的时候,我们都需要实现两个协议:

  • UITableViewControllerDataSource
  • UITableViewControllerDelegate

这两个协议的代码不是写在Controller里就是写在ViewModel里,并且这些方法很难复用。关于Controller瘦身的更多细节,可以参我之前的一篇博客:

  • MVVM与Controller瘦身实践

是否有一种更好的方式来开发TableView呢?如果是Model驱动,而不是代理方法驱动的就好了,如果是Model驱动,开发的时候只需要:

  • 创建Row和Section对应的Model
  • 由一个Manager去管理这些Model,并且对Client隐藏DataSource和Delegate方法
  • 把TableView绑定到Manager

基于这些理念,开发了一个model-driven-tableView框架,

  • MDTable

问题

重复代码

Delegate/DataSource中,有许多重复的代码。比如:

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{return _dataArray.count;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{return 80.0;
}

这些代码的逻辑其实是一样的,一个section,一个数组作为Model,row的数量就是数组元素的个数。但是,很多时候我们都是在一个一个Controller之间进行copy/paste。

Render代码

通常,在cellForRowAtIndexPath或者willDisplay中,我们会对Cell进行重新配置,保证cell在复用的时候显示正确。于是,对Cell进行配置的代码耦合到了ViewController里,

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{YourCustomCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];if (cell == nil) {cell = [[YourCustomCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];}cell.leftLabel.text = titleArray[indexPath.row];cell.infoIcon.image = [UIImage imageNamed:imageArray[indexPath.row]];cell.rightLabel.text = rightArray[indexPath.row];return infoCell;
}

大量的if/else

当Cell的种类多了起来,或者点击cell的动作复杂起来,你会发现代码里充斥着各种各样的if/else(switch也一样)。大量的if/else导致代码难以阅读和维护。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {if (section == 0) {}else if(section == 1){}else{}
}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {if (indexPath.section == 0) {}else if(indexPath.section == 1){}else{}
}

这种情况,在设置界面尤其明显,比如这是网易云音乐的设置界面:

思考一下,如果让你来写,你会怎么写?


解决方案

基类

继承是一个实现代码复用的解决方案,通过在基类中实现-子类重写的方式进行服复用。

比如:

@interface SingleSectionTableViewController : UITableViewController
@property (strong, nonatomic)NSMutableArray * dataArray;
@end
@implementation SingleSectionTableViewController#pragma mark - Table view data source- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {return self.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {[NSException raise:@"Com.table.exception" format:@"You must override this method"];return nil;
}
@end

当然,除了这些,你还可以在基类中配置好你的下拉刷新和上拉加载等逻辑。


CellModel

cellForRowAtIndexPath中,写了大量的cell render代码,从MVVM的角度来看,我们可以通过建立CellModel的方式把这部分代码抽离出来。

在开发的时候,要始终牢记单一功能原则。Cell是一个纯粹的View层,那么其对业务应该尽可能的少知道。

我们来看看,引入了CellModel后,如何进行代码的编写:

Model

@interface Person : NSObject
@property (assign, nonatomic) NSUInteger age;
@property (copy, nonatomic) NSString * name;
@property (copy, nonatomic) NSString * city;
@end

ViewModel

@interface CustomCellModel : NSObject
- (instancetype)initWithModel:(Person *)person;
@property (strong, nonatomic) Person * person;
@property (assign, nonatomic) NSString * nameText;
@property (assign, nonatomic) NSString * ageText;
@end@implementation CustomCellModel
- (instancetype)initWithModel:(Person *)person{if (self = [super init]) {self.person = person;self.nameText = person.name;self.ageText = [NSString stringWithFormat:@"%ld",(long)person.age];}return self;
}
@end

View

@interface CustomTableViewCell : UITableViewCell
@property (strong, nonatomic) UILabel * nameLabel;
@property (strong, nonatomic) UILabel * ageLabel;
@property (strong, nonatomic) CustomCellModel * cellModel;
- (void)bindWithCellModel:(CustomCellModel *)cellModel;
@end@implementation CustomTableViewCell
- (void)bindWithCellModel:(CustomCellModel *)cellModel{self.cellModel = cellModel;self.nameLabel.text = cellModel.nameText;self.ageLabel.text = cellModel.ageText;
}
@end

这时候,Controller中的代码

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {CustomTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];Person * model = [self.dataArray objectAtIndex:indexPath.row];CustomCellModel * cellModel = [[CustomCellModel alloc] initWithModel:model];[cell bindWithCellModel:cellModel];return nil;
}

可以看到,引入了Cell Model后,

  • View和Model十分纯粹,view只做展示,Modle只做业务的模型化
  • ViewModel层作为View与Model的枢纽,把Model层的数据转换成View层需要显示用的数据
  • Controller根据Model合成ViewModel,并且绑定给View

当代码和业务复杂起来的时候,你会发现引入了ViewModel让你的工程更清晰,也更容易测试和维护

Note:MVVM中有两个原则一定要遵守,否则就不是MVVM

  • View持有ViewModel的引用,反之不持有
  • ViewModel持有Model的引用,反之不持有

Dispatch

还记得那令人恶心的一大堆if/else么?那么,iOS开发中有什么更好的方式来实现这个机制呢?

这里,以selector的方式来解决didClickRowAtIndexPath.

定义一个协议,来表示ViewModel可以用来进行方法dispatch

@protocol CellActionDispatchable <NSObject>
@property (copy, nonatomic) NSString * selNameForDidSelected;
@end

然后,让ViewModel遵循这个协议,并且提供SEL的name:

@interface CustomCellModel : NSObject<CellActionDispatchable>
@property (copy, nonatomic) NSString * selNameForDidSelected;
@end

然后,在didSelectRowAtIndexPath中,执行这个SEL

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{[tableView deselectRowAtIndexPath:indexPath animated:true];id<CellActionDispatchable> cellModel = [self.dataArray objectAtIndex:indexPath.row];NSString * selName = cellModel.selNameForDidSelected;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"[self performSelector:NSSelectorFromString(selName) withObject:indexPath];
#pragma clang diagnostic pop
}

抽离数据源

这部分参考自:

  • objc.io: 更轻量的 View Controllers

抽离数据源是通过把对应的通用逻辑抽离出来,比如对于一个单一的ArrayDataSource:

@implementation SingleSectionDataSource+ (instancetype)dataSourceWithData:(NSArray *)dataArrayreuseIdentifier:(NSString *)reuseIdentifieronRender:(void (^)(UITableViewCell *, id))renderBlock{SingleSectionDataSource * ds = [[SingleSectionDataSource alloc] init];ds.dataArray = dataArray;ds.renderBlock = renderBlock;ds.reuseIdentifier = reuseIdentifier;return ds;
}- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{return self.dataArray.count;
}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:self.reuseIdentifier forIndexPath:indexPath];if (self.renderBlock) {id item = [self itemAtIndexPath:indexPath];self.renderBlock(cell, item);}return cell;
}- (id)itemAtIndexPath:(NSIndexPath *)indexPath{return self.dataArray[indexPath.row];
}
@end

然后,就可以这么使用:

// self.navigationItem.rightBarButtonItem = self.editButtonItem;
self.customDataSource = [SingleSectionDataSource dataSourceWithData:dataArrayreuseIdentifier:"cell"onRender:^(UITableViewCell *, id item) {}];
self.tableView.dataSource = self.customDataSource;

MDTable

MDTable是一个模型驱动的响应式框架,使用MDTable,开发者不需要关注复杂的Delegate/DataSource方法。MDTable只关注三件事情

  • Row - 用来表示每一行的模型。你可以选择继承ReactiveRow基类(更简单),或者实现协议RowConvertable
  • Section - 实现SectionConvertable协议的实例,用来表示每一个Section,使用框架提供的Section类型即可。
  • Cell - MDTableViewCell及其子类,用来表示每一行如何展示。

在使用MDTable的时候,开发者只需要

  • 根据数据生成RowModel和SectionModel
  • 根据RowModel和SectionModel创建Manager
  • 把Manager绑定到TableView。
//创建Row
let row0_0 = SystemRow(title: "System Cell", accessoryType: .disclosureIndicator)
let row0_1 = SystemRow(title: "Custom Cell", accessoryType: .disclosureIndicator)
//创建Section
let section0 = SystemSection(rows: [row0_0,row0_1]])
section0.titleForHeader = "Basic"
section0.heightForHeader = 30.0
//创建Manager
tableView.manager = TableManager(sections: [section0,section1])

Cell

为了能够让子类重写,MDTable提供了MDTableViewCell(对UITableViewCell的简单封装)。并且提供了类Row来表示SystemTableViewCell对应的Model。

  • image
  • title
  • detailTitle
  • accessoryType
  • rowHeight
  • cellStyle
  • reuseIdentifier 复用标识符
  • initalType 初始化类型(通过xib/还是代码)

事件

MDTable采用响应式的API来进行事件回调:

row.onWillDisplay { (tableView, cell, indexPath) in//Access manager with tableView.manager
}
row.onDidSelected { (tableView, indexPath) in}

自定义Cell

自定义Cell,你需要以下两个步骤:

创建Model类

创建一个类型,继承ReactiveRow

class XibRow:ReactiveRow{//Datavar title:Stringvar subTitle:Stringvar image:UIImageinit(title:String, subTitle:String, image:UIImage) {self.title = titleself.subTitle = subTitleself.image = imagesuper.init()self.rowHeight = 80.0self.reuseIdentifier = "XibRow"self.initalType = RowConvertableInitalType.xib(xibName: "CusomCellWithXib")}
}

创建MDTableViewCell的子类

可以用XIB,或者Class。只要与RowModel的initalType一致即可。然后,重写Render方法

class CusomCellWithXib: MDTableViewCell{    override func render(with row: TableRow) {guard let row = row as? XibRow else{return;}//Render the cell }
}

接着,在Controller中,使用RowModel即可:

import MDTableclass CustomCellWithXibController: UITableViewController {override func viewDidLoad() {super.viewDidLoad()navigationItem.title = "Custom cell with XIB"let rows = (1..<100).map { (index) -> CustomXibRow inlet row = CustomXibRow(title: "Title\(index)", subTitle: "Subtitle \(index)", image: UIImage(named: "avatar")!)row.didSelectRowAt = { (tableView, indexPath) intableView.manager.delete(row: indexPath)tableView.deleteRows(at: [indexPath], with: .automatic)}return row}let section = SystemSection(rows: rows)section.heightForHeader = 30.0section.titleForHeader = "Tap Row to Delete"tableView.manager = TableManager(sections: [section])}
}

动态行高

由于行高是在RowModel里提供的,所以你需要在这里动态计算行高

   var rowHeight: CGFloat{get{let attributes = [NSFontAttributeName: CustomCellWithCodeConfig.font]let size = CGSize(width: CustomCellWithCodeConfig.cellWidth, height: .greatestFiniteMagnitude)let height = (self.title as NSString).boundingRect(with: size,options: [.usesLineFragmentOrigin],attributes: attributes,context: nil).size.heightreturn height + 8.0}}

由于是模型驱动的,你可以对行高进行缓存或者预计算来让UI更丝滑。


编辑

如果某一行支持编辑,那么它需要实现协议EditableRow

class SwipteToDeleteRow: ReactiveRow, EditableRow{var titleForDeleteConfirmationButton: String? = "Delete"var editingStyle:UITableViewCellEditingStyle = .delete
}

MDTable提供了Editor(协议)来处理编辑相关的逻辑,并且提供了一个默认的TableEditor

比如,最简单的滑动删除

let rows = (1..<100).map { (index) -> SwipteToDeleteRow inlet row = SwipteToDeleteRow(title: "\(index)")return row
}
let section = Section(rows: rows)
section.heightForHeader = 30.0
section.titleForHeader = "Swipe to Delete"
let tableEditor = TableEditor()
tableEditor.editingStyleCommitForRowAt = { (tableView, style, indexPath) inif style == .delete{tableView.manager.delete(row: indexPath)tableView.deleteRows(at: [indexPath], with: .automatic)}
}
tableView.manager = TableManager(sections: [section],editor:tableEditor)

排序

排序也在EditableRow协议中

class ReorderRow: ReactiveRow, EditableRow{var titleForDeleteConfirmationButton: String? = nilvar editingStyle:UITableViewCellEditingStyle = .nonevar canMove: Bool = truevar shouldIndentWhileEditing: Bool = false
}

同样,你需要创建一个TableEditor,来管理排序相关的逻辑:

tableView.setEditing(true, animated: false)
let rows = (1..<100).map { (index) -> ReorderRow inlet row = ReorderRow(title: "\(index)")return row
}
let section = Section(rows: rows)
section.heightForHeader = 0.0
let tableEditor = TableEditor()
tableEditor.moveRowAtSourceIndexPathToDestinationIndexPath = { (tableview,sourceIndexPath,destinationIndexPath) intableview.manager.exchange(sourceIndexPath, with: destinationIndexPath)
}
tableView.manager = TableManager(sections: [section],editor:tableEditor)

Index Title

IndexTitle的实现非常容易,只需要配置Section的sectionIndexTitle属性即可

section.sectionIndexTitle = "A"

总结

TableView的deleage/dataSource方法让开发变的很灵活,却也让代码变的很丑陋。MDTable是笔者一次封装实践,源码地址:

  • MDTable

http://chatgpt.dhexx.cn/article/rh4CbUTg.shtml

相关文章

JavaFX控件——TableView

在JavaFX 应用中对创建表格最重要的是TableView, TableColumn和TableCell这三个类。 你可以通过实现数据模型&#xff08;data model&#xff09; 和 实现 单元格工厂(cell factory) 来填充表格。 表格类提供了表格列嵌入式的排序能力和必要时调整列宽度的功能。 下面开始学…

ios开发:多个Section的TableView

开发多个Section的tableView。 首先应该考虑到数据源该如何得到 我们这里可以通过两种方式:第一种是读取plist文件。第二种是通过代码进行数据存储以及读取。 多个Section需要的数据源是一个字典&#xff0c;字典里的内容是一个数组。在plist文件中可以这样去创建 在.h文件中…

tableview概述

转自&#xff1a;http://www.cnblogs.com/smileEvday/archive/2012/06/28/tableView.html                 下面分9个方面进行介绍&#xff1a; 一、UITableView概述 UITableView继承自UIScrollView&#xff0c;可以表现为Plain和Grouped两种风格&#xff0c;分…

ios tableView那些事(一)创建一个简单的tableView

工作也有半年多了&#xff01;几乎每个项目中的会用到tableview这个神奇而好用的控件&#xff0c;在学习和工作中都会看别人的博客&#xff01;对我有很大的帮助&#xff0c;就如同站在巨人的肩膀上的感觉吧 哈哈&#xff01;于是决定重新开始写博客&#xff0c;希望能帮助像我…

JavaFX TableView和ListView的点击事件

项目场景&#xff1a; 最近在用JavaFX做一个简易的商城界面&#xff0c;大概想实现这样的功能&#xff1a; 左边显示用户的最近五个购买的产品 使用ListView 点击ListView的项目会定位到相应的tablerow位置 方便用户快速查找中间显示所有可用产品 使用TableView 双击tablerow…

JavaFX【TableView使用详解】

目录 概述 组件 Student ObservableList TableView setCellValueFactory() TableColumn 1. Callback 2. PropertyValueFactory 增加到末行 1、tableView.getItems().add(Student s) 2、list.add(Student s) 删除指定行 1、tableView.getItems().remove(int i) 2、…

QT中TableView数据展示

QT中TableView数据展示 最近在学习QT,大量数据从数据库取出放入QT界面中展示&#xff0c;这时用到了tableView&#xff0c;一些简单的使用分享给大家。 创建数据模型 QStandardItemModel *modelnew QStandardItemModel(); QStandardItemModel是Qt库中的一个类&#xff0c;它…

JAVAFX的TableView基本用法

JAVAFX中的表格显示主要使用TableView 与TableView相关的类: TableColumn TableRow TableCell TablePosition TableViewFocusModel TableViewSelectionModel JavaFX TableView例子: import javafx.application.Application; import javafx.scene.Scene; import javafx.scene…

QT之Tableview

想要了解更多的tableview可以看这位博客Qt深入浅出&#xff08;十五&#xff09;QTableView​ 这里做了一个简单的学生系统查询功能显示Tableview&#xff1a; 表格视图控件QTableView&#xff0c;需要和QStandardItemModel, 配套使用&#xff0c;这套框架是基于MVC设计模式设…

QML TableView 使用详解

目录 一、—个简单的TableView实例 二、TableViewColumn 属性讲解 三、定制表格外观 3.1 itemDelegate3.2 rowDelegate3.3 headerDelegate3.4 定制表格外观后的示例 四、动态增删改查 TabelView TableView 就是 Qt Quick 为表格式呈现数据提供的组件。想必兄台用过 Excel…

QT控件之(TableView)的居中效果

T将tableView中的表头以及文本内容都进行居中处理 1、需要在构造函数中增加一句&#xff1a; //以下增加的是表头的效果 ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);//布局排版是全部伸展开的效果2、就是直接对tableView的文本内…

QML学习十七:TableView的简单使用

若该文为原创文章&#xff0c;转载请注明原文出处 一、TableView TableView是Qt Quick为表格式呈现数据提供的组件。 TableView与ListView类似&#xff0c;相比之下多了滚动条、挑选、可调整尺寸的表头等特性&#xff0c;数据也是通过Model来提供&#xff0c;此篇使用的是内建…

JavaFX中TableView的使用

稍微说说JavaFX里面TableView怎么用&#xff0c;&#xff08;其实在JavaFX的源码中都有示例。。。&#xff09; 首先要了解TableView是用来做什么的&#xff0c;TableView是JavaFX的一个表视图&#xff0c;用来显示表格的&#xff0c;在TableView的类注释中写了 /*** see Tab…

Qt4实现TableView显示表格数据

最近又开始搞QT开发了&#xff0c;前面学的MVC啥的都忘得差不多了&#xff0c;重新整理一遍思路吧。 目前的需求是&#xff1a;读取文本文件&#xff0c;表格型数据&#xff0c;用tableview显示出来&#xff0c;最后画图。这涉及到三个问题&#xff0c;文件读写&#xff0c;数…

NAT模式下的虚拟机网络配置

原理 NAT模式&#xff0c;也叫地址转换模式&#xff0c; 当把我们的虚拟机的上网方式设置为NAT模式时&#xff0c;虚拟机、宿主机、各网卡之间的连接关系可用下图来表示&#xff1a; 具体配置流程 1 将本地以太网共享到v8适配器上 2 查看虚拟网络编辑中的NAT模式下的子网IP…

虚拟机vmware设置nat模式上网

桥接模式上网&#xff1a;虚拟机vmware设置桥接模式上网_cao849861802的博客-CSDN博客 首先虚拟机有两个虚拟网卡vmnet0和vmnet8 这个vmnet0默认的是桥接模式&#xff0c;这个vmnet8默认是nat模式&#xff1b; 我们这里只看nat模式&#xff0c;所以先不关心vmnet0虚拟网卡&a…

VMware16NAT模式配置固定IP

文章目录 前言一、NAT配置固定IP二、重启网卡结尾 前言 为什么要配置固定IP呀&#xff1f;这个很容易解释啊&#xff0c;因为配置集群要设置固定IP&#xff08;主结点需要管理子结点&#xff0c;通过固定IP识别机器&#xff09;&#xff0c;因为你访问虚拟机方便&#xff08;不…

VMware Workstation中桥接模式、NAT模式、仅主机模式

一、VMware虚拟机的网络模式 VMware工作站虚拟机有三种网络模式【①桥接模式 ②NAT模式 ③仅主机模式】,如下图所示: 二、VMware虚拟机的网络模式介绍 2.0、VMware的虚拟设备 VMware的虚拟设备 序号虚拟设备编号说明1VMnet0是虚拟桥接网络下的虚拟交换机2VMnet1是虚拟Host-…

vm虚拟机nat模式配置

痛点&#xff1a; 为了解决虚拟机与板子网络的调试的问题&#xff0c;我之前一直用桥接&#xff0c;如果虚拟机想上网就桥接到无线网卡&#xff0c;如果想连接板子&#xff0c;就桥接到有线网卡&#xff0c;麻烦&#xff0c;需要来回切换&#xff0c;还有就是不插板子的情况下和…

nat模式

原文链接&#xff1a;https://www.linuxidc.com/Linux/2016-09/135521p2.htm &#xff08;复制过来只是为了学习方便&#xff0c;如有不妥会立即删除&#xff09; 二、NAT&#xff08;地址转换模式&#xff09; 刚刚我们说到&#xff0c;如果你的网络ip资源紧缺&#xff0c;但…