iOS开发基础12-深入理解UITableView(一)
在 iOS 应用开发中,UITableView
是展示列表数据的常用控件。本文将详细介绍 UITableView
的工作原理、数据展示、Cell 的重用机制、自定义 Cell 以及如何通过 UITableView 展示数据。
一、什么是 UITableView
在 iOS 中,UITableView
是用来展示列表数据的主要控件。它继承自 UIScrollView
,因此天然支持垂直滚动,同时具备极佳的性能。
UITableView 的两种样式
UITableViewStylePlain
:标准的列表样式,所有单元格都在一个连续的平面上。UITableViewStyleGrouped
:分组样式,每个组有独立的分隔符或背景。
二、如何展示数据
数据源(DataSource)
UITableView
通过一个数据源对象来获取数据。数据源对象必须遵循 UITableViewDataSource
协议,这样 UITableView
才能获取数据进行展示。
数据展示的过程如下:
- 调用数据源方法
numberOfSectionsInTableView:
获取有多少个分组。 - 调用数据源方法
tableView:numberOfRowsInSection:
获取每个分组中的行数。 - 调用数据源方法
tableView:cellForRowAtIndexPath:
获取每一行的内容。
示例代码
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// 返回分组数量
return [self.dataArray count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// 返回每个分组中的行数
return [[self.dataArray objectAtIndex:section] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 获取某一行的数据并返回对应的Cell
static NSString *cellIdentifier = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
}
// 配置cell内容
cell.textLabel.text = [NSString stringWithFormat:@"Section %ld Row %ld", (long)indexPath.section, (long)indexPath.row];
return cell;
}
三、Cell简介
UITableView
的每一行都是一个 UITableViewCell
,通过数据源中的 tableView:cellForRowAtIndexPath:
方法来创建和初始化每一行。
UITableViewCell 的辅助指示视图
每个 UITableViewCell
都有一个 contentView
,这是用于显示内容的父视图。你可以通过 accessoryType
属性来设置辅助图标,包括:
UITableViewCellAccessoryNone
:不显示图标。UITableViewCellAccessoryDisclosureIndicator
:显示箭头(通常表示可以点击查看详细信息)。UITableViewCellAccessoryDetailButton
:显示信息按钮。UITableViewCellAccessoryDetailDisclosureButton
:显示详细信息按钮和箭头。UITableViewCellAccessoryCheckmark
:显示对勾。
还可以通过 accessoryView
属性来自定义辅助指示视图。
ContentView 子视图
contentView
下默认有三个子视图:
- 两个
UILabel
,可以通过textLabel
和detailTextLabel
属性访问。 - 一个
UIImageView
,可以通过imageView
属性访问。
可以通过 UITableViewCellStyle
属性来决定子视图的布局和样式:
UITableViewCellStyleDefault
UITableViewCellStyleSubtitle
UITableViewCellStyleValue1
UITableViewCellStyleValue2
四、Cell的重用原理
由于内存限制,创建大量 UITableViewCell
会耗尽设备内存。为了提升性能,UITableView
提供了 Cell 重用机制。
重用原理
当列表滚动时,移出屏幕的 UITableViewCell
会放入重用池中。调用 dequeueReusableCellWithIdentifier:
方法可以从重用池中取出已存在的 Cell,这样可以避免重复创建,节省资源。
重用代码示例
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
// 配置cell内容
cell.textLabel.text = [NSString stringWithFormat:@"Section %ld Row %ld", (long)indexPath.section, (long)indexPath.row];
return cell;
}
不同类型的 Cell 重用
如果一个 UITableView
中有不同类型的 Cell,可以通过设置不同的 reuseIdentifier
来区分每种类型的 Cell,从而避免类型错误。
五、通过代码自定义 Cell
当需要自定义 UITableViewCell
时,可以通过继承 UITableViewCell
来实现个性化展示。
自定义 Cell 的步骤
- 新建一个继承自
UITableViewCell
的类。 - 重写
initWithStyle:reuseIdentifier:
方法,添加需要显示的子控件。 - 提供两个模型:数据模型和框架模型。
Cell
拥有一个框架模型,重写框架模型的setter
方法,在该方法中设置子控件的数据和框架。
示例代码
// 自定义Cell的头文件
@interface CustomTableViewCell : UITableViewCell
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIImageView *customImageView;
@end
// 自定义Cell的实现文件
@implementation CustomTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.titleLabel = [[UILabel alloc] init];
[self.contentView addSubview:self.titleLabel];
self.customImageView = [[UIImageView alloc] init];
[self.contentView addSubview:self.customImageView];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
// 设置子控件的 frame
self.titleLabel.frame = CGRectMake(10, 10, 200, 30);
self.customImageView.frame = CGRectMake(self.contentView.frame.size.width - 40, 10, 30, 30);
}
@end
// 使用自定义Cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *customCellIdentifier = @"customCell";
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:customCellIdentifier];
if (!cell) {
cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:customCellIdentifier];
}
cell.titleLabel.text = @"Custom Cell";
cell.customImageView.image = [UIImage imageNamed:@"example"];
return cell;
}
六、利用 UITableView 展示汽车数据案例
通过一个具体的示例来展示如何使用 UITableView
展示复杂数据。
示例代码
#import "ViewController.h"
#import "CarGroup.h"
#import "Car.h"
#define cellIdentifier @"carCell"
@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, copy) NSArray *carGroups;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 设置表格的样式和数据源
self.tableView.dataSource = self;
self.tableView.delegate = self;
// 设置右侧索引文字的颜色和背景色
self.tableView.sectionIndexColor = [UIColor redColor];
self.tableView.sectionIndexBackgroundColor = [UIColor blackColor];
// 注册Cell
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:cellIdentifier];
}
- (NSArray *)carGroups {
if (!_carGroups) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"cars" ofType:@"plist"];
NSArray *carGroupsDictArray = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *carGroupsM = [NSMutableArray array];
for (NSDictionary *dict in carGroupsDictArray) {
CarGroup *group = [CarGroup carGroupWithDict:dict];
[carGroupsM addObject:group];
}
_carGroups = carGroupsM;
}
return _carGroups;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.carGroups.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
CarGroup *group = self.carGroups[section];
return group.cars.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
CarGroup *group = self.carGroups[indexPath.section];
Car *car = group.cars[indexPath.row];
cell.textLabel.text = car.name;
cell.imageView.image = [UIImage imageNamed:car.icon];
return cell;
}
#pragma mark - UITableViewDelegate
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
CarGroup *group = self.carGroups[section];
return group.title;
}
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return [self.carGroups valueForKeyPath:@"title"];
}
@end
// CarGroup.h
@interface CarGroup : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSArray *cars;
+ (instancetype)carGroupWithDict:(NSDictionary *)dict;
@end
// CarGroup.m
#import "CarGroup.h"
#import "Car.h"
@implementation CarGroup
+ (instancetype)carGroupWithDict:(NSDictionary *)dict {
CarGroup *group = [[self alloc] init];
group.title = dict[@"title"];
NSMutableArray *cars = [NSMutableArray array];
for (NSDictionary *carDict in dict[@"cars"]) {
Car *car = [Car carWithDict:carDict];
[cars addObject:car];
}
group.cars = cars;
return group;
}
@end
// Car.h
@interface Car : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *icon;
+ (instancetype)carWithDict:(NSDictionary *)dict;
@end
// Car.m
#import "Car.h"
@implementation Car
+ (instancetype)carWithDict:(NSDictionary *)dict {
Car *car = [[self alloc] init];
[car setValuesForKeysWithDictionary:dict];
return car;
}
@end
底层逻辑分析
在这个示例中,我们从 plist 文件中读取汽车数据,然后通过模型类 CarGroup
和 Car
来组织这些数据。通过重写 UITableViewDataSource
和 UITableViewDelegate
方法,我们将数据展示在 UITableView
上。
- 数据模型:通过模型类将原始数据转化为可操作的数据对象,方便后续的操作和展示。
- 数据展示:通过实现
UITableViewDataSource
和UITableViewDelegate
方法,将数据源和视图控制器连接起来,完成数据的展示和交互操作。
七、Cell不等高
在 UITableView
中,如果每个 UITableViewCell
的高度都相同,可以显著简化实现逻辑。然而,有时候我们需要实现每个 Cell 高度不一样的情况。下面介绍几种实现不等高 UITableView
的做法。
方法一:实现 tableView:heightForRowAtIndexPath:
这是最常用的方法之一,通过实现 UITableViewDelegate
协议中的 tableView:heightForRowAtIndexPath:
方法,可以设置每个 Cell 的高度。
示例代码
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// 根据 indexPath 返回对应的 Cell 高度
if (indexPath.row % 2 == 0) {
return 60.0; // 偶数行高度为60
} else {
return 100.0; // 奇数行高度为100
}
}
逻辑分析
这个方法允许针对每个 Cell 直接返回不同的高度。实现简单且直观,但如果需要计算复杂的高度还需要一些额外的准备工作,例如提前计算好每个 Cell 需要的高度。
方法二:使用自动布局和估算高度
自 iOS 8 开始,UITableView
支持自动布局(Auto Layout)计算 Cell 高度,同时利用估算高度(estimatedRowHeight)来提升性能。
- 启用自动布局
- 设置估算高度
示例代码
- (void)viewDidLoad {
[super viewDidLoad];
// 启用自动布局
self.tableView.estimatedRowHeight = 80.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
}
在 Cell 配置中,确保所有子视图都有明确的 Auto Layout 约束,以支持自动高度计算。
配置 Cell
在自定义的 UITableViewCell
中,采用自动布局配置子视图。例如:
// CustomTableViewCell.h
@interface CustomTableViewCell : UITableViewCell
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *detailLabel;
@end
// CustomTableViewCell.m
@implementation CustomTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.numberOfLines = 0; // 多行显示
[self.contentView addSubview:self.titleLabel];
self.detailLabel = [[UILabel alloc] init];
self.detailLabel.numberOfLines = 0; // 多行显示
[self.contentView addSubview:self.detailLabel];
// 添加约束
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.detailLabel.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[self.titleLabel.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:10],
[self.titleLabel.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:10],
[self.titleLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-10],
[self.detailLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:10],
[self.detailLabel.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:10],
[self.detailLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-10],
[self.detailLabel.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-10]
]];
}
return self;
}
@end
逻辑分析
- 自动布局:利用自动布局,系统会根据设置好的约束动态计算 Cell 的高度。
- 估算高度:设置估算高度可以让
UITableView
提前计算好部分内容的高度,从而提高滚动性能和加载速度。
方法三:预计算高度并缓存
如果计算不同 Cell 的高度比较复杂,可能导致滚动卡顿问题。此时,可以预先计算好每个 Cell 的高度并进行缓存。
示例代码
- 定义高度缓存
@property (nonatomic, strong) NSMutableDictionary *heightCache;
@property (nonatomic, strong) NSArray *dataArray; // 假设数据源是这个
- 在
tableView:heightForRowAtIndexPath:
方法中使用缓存
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *cachedHeight = self.heightCache[@(indexPath.row)];
if (cachedHeight) {
return cachedHeight.floatValue;
} else {
CGFloat calculatedHeight = [self calculateHeightForIndexPath:indexPath];
self.heightCache[@(indexPath.row)] = @(calculatedHeight);
return calculatedHeight;
}
}
- 计算高度
在 calculateHeightForIndexPath:
方法中实现复杂的高度计算逻辑,例如根据文本内容来计算高度:
- (CGFloat)calculateHeightForIndexPath:(NSIndexPath *)indexPath {
// 根据文本内容和预制的字体样式计算文本所需的高度
NSString *text = self.dataArray[indexPath.row];
CGSize maxSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, CGFLOAT_MAX); // 最大宽度限制
CGRect textRect = [text boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:17]}
context:nil];
return textRect.size.height + 20.0; // 增加一些内边距
}
逻辑分析
- 高度计算:提前计算好并缓存高度可以显著提升
UITableView
的性能,避免频繁的高度计算。 - 缓存机制:通过字典缓存每行的高度,减少重复计算。
结论
上述方法从多角度解决了 UITableView
中不等高 Cell 的实现。选择合适的方法取决于具体的应用场景和性能需求:
- 简单情景:直接实现
tableView:heightForRowAtIndexPath:
。 - 多样化内容且支持自动布局:使用
UITableViewAutomaticDimension
和估算高度。 - 高度计算复杂且需要优化性能:提前计算并缓存 Cell 高度。
本文详细介绍了 UITableView
的基本概念、如何展示数据、Cell 的基本使用和高级自定义、以及 Cell 重用机制。通过一个具体的案例展示了如何在 UITableView
中展示数据。
掌握了这些知识点之后,你将能更加高效地使用 UITableView
来构建数据驱动的 iOS 应用。