UITableView 是 iOS 上最常用的组件了,几乎每个应用中都会用到。像 iOS 的「设置」应用,就几乎全部由 UITableView 组成。
UITableView 的组成
一个 TableView 里有一个或多个 Section,Section 里包含了很多个 Cell(UITableViewCell),图上 Section 里每行都是一个 Cell,除此之外,每个 Section 还包含了一个 Header View 和一个 Footer View。
UITableView 的基本用法
在 Storyboard 中,我们可以直接将一个 Table View Controller 拖动到视图中来使用,这是系统帮我们实现好的一个 UITableViewController,不过这里,我们还是用另一种比较麻烦的方式来实现,这样可以帮助我们更好地理解 UITableView 的结构和使用方法。
新建一个基于 Storyboard 的 iOS 应用,然后在 Storyboard 中,我们在 Object library 中找到 Table View,然后拖到我们的视图中,这个时候运行应用是没有什么问题的,只是界面上会是一个空白的 TableView,因为 UITableView 要显示数据,要为其提供数据源(Data Source),要响应事件,我们还要为其提供 Delegate。
在 Storyboard 中为 UITableView 添加数据源和 delegate 很简单,只需在其 Connections Inspector 中将 dataSource 和 delegate 分别拖向实现了 UITableViewDataSource 和 UITableViewDelegate 的类中即可。
在这里,我们选择模仿 UITableViewController 的实现方法:
将 dataSource 和 delegate 链接到其 View Controller 中:
然后在 View Controller 中实现
UITableViewDataSource和UITableViewDelegate协议:@interface ViewController : UIViewController<UITableViewDataSource, UITableViewDelegate> @end在 ViewController 中实现必要的和可选的方法。
UITableViewDataSource中有两个@required属性的方法是必须要实现的:- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; // Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier: // Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls) - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;UITableViewDelegate里的方法均为@optional。
UITableViewDataSource
- numberOfSectionsInTableView:// 返回TableView里一共有几个Section,不重写的话默认返回1 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 2; }- tableView:numberOfRowsInSection:// Section中有几行Rows(UITableViewCell) // 参数section表示第几个Section,从0开始计数 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section == 0) { return 3; } else if (section == 1) { return 2; } return 1; }tableView:cellForRowAtIndexPath:// 返回一个用于被展示的Cell // 参数 indexPath 里包含了 section 和 row 信息 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell"]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MyCell"]; } if (indexPath.section == 0) { cell.imageView.image = [UIImage imageNamed:@"gear"]; } else if (indexPath.section == 1) { cell.imageView.image = [UIImage imageNamed:@"guitar"]; } cell.textLabel.text = [NSString stringWithFormat:@"%ld", indexPath.row]; cell.detailTextLabel.text = [NSString stringWithFormat:@"Section: %ld - Row: %ld", indexPath.section, indexPath.row]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; return cell; }- tableView:viewForHeader/FooterInSection://根据 Section 值返回 Section 的 Header 或 Footer 的值 - (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { UIView* header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"HeaderOrFooter"]; if (!header) { header = [[UIView alloc] init]; } if (section == 0) { header.backgroundColor = [UIColor lightGrayColor]; } else if (section == 1) { header.backgroundColor = [UIColor darkGrayColor]; } return header; } - (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { UIView* footer = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"HeaderOrFooter"]; if (!footer) { footer = [[UIView alloc] init]; } if (section == 0) { footer.backgroundColor = [UIColor blueColor]; } else if (section == 1) { footer.backgroundColor = [UIColor cyanColor]; } return footer; }
UITableViewDelegate
- tableView:didSelectRowAtIndexPath:// 行被选中时执行的方法 // 参数indexPath里包含了section 和 row信息 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSLog(@"%@", [NSString stringWithFormat:@"Section: %ld - Row: %ld", indexPath.section, indexPath.row]); }
- tableView:didDeselectRowAtIndexPath:// 行被取消选中时执行的方法,如选中新一行的时候,原来被选中的行就会执行此方法 // 参数indexPath里包含了section 和 row信息 - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath { NSLog(@"%@", [NSString stringWithFormat:@"Section: %ld - Row: %ld", indexPath.section, indexPath.row]); }
UITableViewCell 的使用
如果每需要一个 UITableViewCell 就新建一个,那么当一个 TableView 中行数很多时,就会对系统造成巨大的内存压力,所以我们一般在 tableView:cellForRowAtIndexPath: 等方法中,会先检查看有没有已经创建好的且当前没在使用的 Cell,有就直接拿来用,没有的话就新创建一个。
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell Identifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"Cell Identifier"];
}系统提供的 UITableViewCell
TableView Cell 定制
Cell 的高度
- tableView:heightForRowAtIndexPath:这个方法会在每次 TableView 显示前挨个询问 TableViewCell 的高度,如果 TableView 中行数太多的话,开销会比较大。
tableView.rowHeight如果我们确定 tableView 中每一行的高度都是一样的,那个可以在 tableView 中设置:
tableView.rowHeight = 100;因为
- tableView:heightForRowAtIndexPath:方法优先级比较高,所以在设置这个属性后就不应该再提供- tableView:heightForRowAtIndexPath:方法了,否则会没有效果。rowHeight属性默认值为UITableViewAutomaticDimension,系统会自动计算 Cell 的高度,除非已经定义了 Cell的高度(如:Storyboard 中)。- tableView:estimatedHeightForRowAtIndexPath:// Use the estimatedHeight methods to quickly calcuate guessed values which will allow for fast load times of the table.
// If these methods are implemented, the above -tableView:heightForXXX calls will be deferred until views are ready to be displayed, so more expensive logic can be placed there.提供一个估算的值,这样,
- tableView:heightForRowAtIndexPath:方法的调用会推迟到 Cell(不是 TableView)即将被显示时。
可视化定制 Cell - Prototype Cell
首先将 TableView 的 Content 属性设置为 Dynamic Prototypes,然后在 Object library 中找到 Table View Cell,然后将其拖至 TableView 中,然后定制其即可,这样就创建好了一个 Cell 模板。
使用时:
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell Identifier"];这样就可以获得一个 Cell 了,这种况下其不会返回 nil,如果没有会自动创建一个然后返回。
我们可以通过 - viewWithTag: 方法来获取 cell 内部的控件。
这么做会有些麻烦,因为获取过来后还是需要强制类型转换后才能使用。
我们可以在创建 Cell 模板的时候,将 Cell 的 Class 设置成自定义的 Class(集成自 UITableViewCell ),然后把控件链接到 IBOutlet 上,并暴露出来,这样我们就可以方便的访问其控件了:
CustomCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Custom Cell Identifier"];
// customLabel 为链接到空间上的 IBOutlet 属性
cell.customLabel.text = @"Whatever";可视化定制 Cell - 从 Xib 中加载
New->File,选择 Cocoa Touch Class,然后创建一个 UITableViewCell 的子类,并勾选 Also create XIB file。
然后在 Xib 文件中定制 Cell,并关联至 Class 中。
使用:
方法一:
// 在合适的地方:如viewDidLoad等处 UINib* nib = [UINib nibWithNamed:@"XibCellFileName" bundle:bundle]; [self.tableView registerNib:nib forCellReuseIdentifier:@"Cell Identifier"];其后的使用和之前一致。
方法二:
// 在合适的地方:如 viewDidLoad 等处 [self.tableView registerClass:[XibCellClass class] forCellReuseIdentifier:@"Cell Identifier"];注意:要使用此方法,需要在 XibCellClass 中实现
- initWithStyle: reuseIdentifier:方法:- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; NSArray *uiObjects = [[BSBundle mainBundle] loadNibNamed:@"XibCellFileName" owner:self options:nil]; self = uiObjects[0];// Find in uiObjects return self; }其后的使用和之前一致。
使用代码控制 UITableView
选中 Row
-[UITableView selectRowAtIndexPath:amimated:scrollPosition:]// 使用代码选中指定行 - (void)selectRowAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition; /* UITableViewScrollPosition 参数 typedef NS_ENUM(NSInteger, UITableViewScrollPosition) { UITableViewScrollPositionNone,// 不自动滚动选中的行 UITableViewScrollPositionTop,// 将选中的行滚动到 TableView 可见区域的顶部 UITableViewScrollPositionMiddle,// 将选中的行滚动到 TableView 可见区域的中间 UITableViewScrollPositionBottom // 将选中的行滚动到 TableView 可见区域的底部 }; */-[UITableView deselectRowAtIndexPath:amimated:]// 使用代码取消选中指定行 - (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated;
读取被选中的行
// 当前选中的行的 IndexPath(单选)
// returns nil or index path representing section and row of selection.
@property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow;
// 当前选中的行的 IndexPath(多选)
// returns nil or a set of index paths representing the sections and rows of the selection.
@property (nonatomic, readonly, nullable) NSArray<NSIndexPath *> *indexPathsForSelectedRows NS_AVAILABLE_IOS(5_0); 控制表格滚动
-[UITableView scrollToRowAtIndexPath:atScrollPosition:animated:]// 滚动指定行至 TableView 的可见区域 - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated; /* UITableViewScrollPosition 参数 在这里 UITableViewScrollPosition 参数和之前选中行时的行为略有不同 typedef NS_ENUM(NSInteger, UITableViewScrollPosition) { UITableViewScrollPositionNone,// 将选中的行就近滚动到 TableView 的可见区域 UITableViewScrollPositionTop, // 将选中的行滚动到 TableView 可见区域的顶部 UITableViewScrollPositionMiddle,// 将选中的行滚动到 TableView 可见区域的中间 UITableViewScrollPositionBottom // 将选中的行滚动到 TableView 可见区域的底部 }; */-[UITableView scrollToNearestSelectedRowAtScrollPosition:scrollPosition:animated:]// 将最近被选中的行滚动至TableView的可见区域 - (void)scrollToNearestSelectedRowAtScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated; /* UITableViewScrollPosition 参数 在这里 UITableViewScrollPosition 参数和之前选中某一行时的行为略有不同 typedef NS_ENUM(NSInteger, UITableViewScrollPosition) { UITableViewScrollPositionNone,// 将选中的行就近滚动到 TableView 的可见区域 UITableViewScrollPositionTop, // 将选中的行滚动到 TableView 可见区域的顶部 UITableViewScrollPositionMiddle,// 将选中的行滚动到 TableView 可见区域的中间 UITableViewScrollPositionBottom // 将选中的行滚动到 TableView 可见区域的底部 }; */
刷新 UITableView
当数据源发生变化时,TableView 并不能知道其已经发生了变化,所以需要我们手动对其刷新。
TableView 的刷新方法有以下几种:
// 刷新整个 TableView
// reloads everything from scratch. redisplays visible rows. because we only keep info about visible rows, this is cheap. will adjust offset if table shrinks
- (void)reloadData;// 仅刷新指定行
- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0); // 仅刷新指定的一个或多个Section
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);// 刷新索引Bar
- (void)reloadSectionIndexTitles NS_AVAILABLE_IOS(3_0);UITableView的编辑模式
UITableView 内建了表格编辑支持,但其仅会对表格进行编辑,数据源需要我们自己更新。
开启表格编辑模式很简单,只需将其 editing 属性设置为 YES 即可开启,关闭时设为 NO 即可。
// 开启编辑模式
self.tableview.editing = YES;
// 关闭编辑模式
self.tableview.editing = NO;
开启编辑模式后,Row 左边就会出现如图所示的红色带减号圆点、绿色带加号圆点以及一些没有圆点的 Row,这些样式就是UITableViewCellEditingStyle,要实现以上效果,我们可以实现 UITableViewDelegate 中的如下几个方法:
// 为指定行提供编辑模式的样式(如上图每行左边的圆点),在编辑模式开启后,TableView 会对其下的所有支持编辑的Row逐一询问。
// 如不重写此方法,则默认返回 UITableViewCellEditingStyleDelete
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath;
/*
返回值
typedef NS_ENUM(NSInteger, UITableViewCellEditingStyle) {
UITableViewCellEditingStyleNone, // 没有标记,显示为空白
UITableViewCellEditingStyleDelete, // 显示红色带减号圆点
UITableViewCellEditingStyleInsert // 显示绿色带加号圆点
};
*///指定行是否支持编辑,在编辑模式开启后,TableView 会对其下的 Row 逐一询问,如果返回 YES,则会访问- tableView:editingStyleForRowAtIndexPath: 询问编辑模式样式
//如不重写此方法,则默认返回 YES
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath;// 响应删除、新建操作,我们可以在此方法中删除、添加 Row,并更新数据源
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath;// 指定行是否支持移动操作,在编辑模式开启后,TableView 会对其下的 Row 逐一询问
// 如不重写此方法,则默认返回 YES
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath;// 提供该方法,编辑模式就会显示移动控件(如编辑模式图中作图所示),在这里可以响应移动操作
// 只有 - tableView:canMoveRowAtIndexPath: 方法返回 YES 的行会显示
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath;添加、删除 Row 及 Section
// 添加、删除 Section
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;// 添加、删除 Row
- (void)insertRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;另外,当我们一次添加/删除多个 Row/Section 时,可以将其放入:
[self.tableview beginUpdates];
//这里
[self.tableview endUpdates];这样,可以通过一次动画完成多次操作。
带索引的表格
// return list of section titles to display in section index view (e.g. "ABCD...Z#")
- (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView __TVOS_PROHIBITED; // fixed font style. use custom view (UILabel) if you want something different
- (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section;// tell table which section corresponds to section title/index (e.g. "B",1))
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index __TVOS_PROHIBITED;
你为何辣么屌