iOS开发基础12-深入理解UITableView(一)
UITableView 深度解析:数据展示、Cell 重用与不等高布局
UITableView 是 iOS 中展示列表数据的核心控件,继承自 UIScrollView,天然支持垂直滚动和高性能的 Cell 重用机制。本文从基础用法到自定义 Cell、不等高布局,全面解析其工作原理。
一、UITableView 概述
1. 什么是 UITableView
UITableView 用于展示一维列表数据,每一行是一个 UITableViewCell。它继承自 UIScrollView,支持垂直滚动,并通过 Cell 重用机制在大量数据下仍保持流畅。
2. 三种样式
| 样式 | 说明 |
|---|---|
UITableViewStylePlain |
标准列表,连续平面,section header 悬浮(iOS 15+ 默认不悬浮,需设置 sectionHeaderTopPadding) |
UITableViewStyleGrouped |
分组样式,每组有独立背景和间距 |
UITableViewStyleInsetGrouped |
iOS 13+ 新增,分组样式且内容横向内嵌(设置 App 风格) |
二、展示数据
1. 数据源(DataSource)
UITableView 通过 dataSource 获取数据,数据源必须遵守 UITableViewDataSource 协议。数据展示按以下顺序调用:
numberOfSectionsInTableView:— 获取分组数(可选,默认 1)。tableView:numberOfRowsInSection:— 获取每组行数。tableView:cellForRowAtIndexPath:— 获取每一行的 Cell。
2. 示例代码
Objective-C
@interface ViewController () <UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic, strong) NSArray *dataArray; // 二维数组:外层 section,内层 row
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
// 注册 Cell(注册后 dequeue 不会返回 nil)
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.dataArray.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.dataArray[section] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 带 indexPath 的 dequeue 方法:注册后保证返回非空 Cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"Section %ld Row %ld",
(long)indexPath.section, (long)indexPath.row];
return cell;
}
@end
Swift
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
var dataArray: [[String]] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func numberOfSections(in tableView: UITableView) -> Int {
return dataArray.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Section \(indexPath.section) Row \(indexPath.row)"
return cell
}
}
两种 dequeue 方法的区别:
dequeueReusableCellWithIdentifier:— 可能返回 nil,需手动判断创建。dequeueReusableCellWithIdentifier:forIndexPath:— 必须提前注册 Cell(registerClass:或registerNib:),保证返回非空,且 Cell 尺寸已根据 TableView 配置确定。推荐使用。
三、UITableViewCell 简介
1. 结构
每个 UITableViewCell 内部有一个 contentView,所有自定义子视图应添加到 contentView 上(而非 cell 本身),因为系统在编辑状态下会调整 contentView 的 frame。
2. 辅助指示视图(Accessory)
通过 accessoryType 设置右侧辅助图标:
| 枚举值 | 显示 | 常用场景 |
|---|---|---|
UITableViewCellAccessoryNone |
无 | 默认 |
UITableViewCellAccessoryDisclosureIndicator |
灰色箭头 | 可点击进入详情页 |
UITableViewCellAccessoryDetailButton |
蓝色信息按钮 | 点击查看详情 |
UITableViewCellAccessoryDetailDisclosureButton |
信息按钮 + 箭头 | 两者结合 |
UITableViewCellAccessoryCheckmark |
对勾 | 选中状态 |
也可通过 accessoryView 属性完全自定义右侧视图。
3. 系统预设样式
contentView 默认包含 textLabel、detailTextLabel(两个 UILabel)和 imageView(一个 UIImageView),通过 UITableViewCellStyle 决定布局:
| 样式 | 布局 |
|---|---|
UITableViewCellStyleDefault |
左图 + 单行文字 |
UITableViewCellStyleSubtitle |
左图 + 标题在上 + 副标题在下 |
UITableViewCellStyleValue1 |
左图 + 标题在左 + 副标题在右(蓝色) |
UITableViewCellStyleValue2 |
标题在左(蓝色小字)+ 副标题在右 |
4. 选中样式
cell.selectionStyle = UITableViewCellSelectionStyleNone; // 无选中效果
cell.selectionStyle = UITableViewCellSelectionStyleDefault; // 默认灰色
cell.selectionStyle = UITableViewCellSelectionStyleGray; // 灰色
cell.selectionStyle = UITableViewCellSelectionStyleBlue; // 蓝色(已废弃效果)
四、Cell 重用原理
1. 为什么需要重用
如果列表有 10000 条数据,创建 10000 个 Cell 会耗尽内存。UITableView 只创建屏幕可见的 Cell(约 10~15 个),滚动时将移出屏幕的 Cell 放入重用池,新进入屏幕的行从重用池取出已有 Cell 重新配置内容。
2. 重用流程
用户滚动 → Cell 移出屏幕 → 放入重用池(按 reuseIdentifier 分类)
→ 新行进入屏幕 → 从重用池取出对应 identifier 的 Cell → 配置新内容 → 显示
3. prepareForReuse
当 Cell 即将被重用时,系统调用 prepareForReuse 方法。应在此方法中重置 Cell 的临时状态(如取消图片加载、清空文本、重置选中状态),避免旧内容残留:
- (void)prepareForReuse {
[super prepareForReuse];
self.imageView.image = nil;
self.textLabel.text = nil;
// 取消正在进行的异步图片加载
}
override func prepareForReuse() {
super.prepareForReuse()
imageView?.image = nil
textLabel?.text = nil
}
4. 多类型 Cell 重用
一个 TableView 中有多种 Cell 时,通过不同的 reuseIdentifier 区分,每种类型独立维护重用池:
if (indexPath.row == 0) {
HeaderCell *cell = [tableView dequeueReusableCellWithIdentifier:@"headerCell" forIndexPath:indexPath];
return cell;
} else {
ContentCell *cell = [tableView dequeueReusableCellWithIdentifier:@"contentCell" forIndexPath:indexPath];
return cell;
}
五、通过代码自定义 Cell
当系统预设样式无法满足需求时,继承 UITableViewCell 自定义。
1. 自定义步骤
- 新建继承自
UITableViewCell的类。 - 重写
initWithStyle:reuseIdentifier:,在contentView上添加子控件。 - 重写
layoutSubviews(手动 frame 布局)或使用 Auto Layout 设置子控件位置。 - 重写
prepareForReuse重置状态。 - 提供模型属性,在 setter 中配置子控件内容。
2. 示例代码
Objective-C
// CustomTableViewCell.h
@interface CustomTableViewCell : UITableViewCell
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIImageView *customImageView;
@property (nonatomic, strong) NJShop *shop;
@end
// CustomTableViewCell.m
@implementation CustomTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.font = [UIFont systemFontOfSize:16];
[self.contentView addSubview:self.titleLabel];
self.customImageView = [[UIImageView alloc] init];
self.customImageView.contentMode = UIViewContentModeScaleAspectFill;
self.customImageView.clipsToBounds = YES;
[self.contentView addSubview:self.customImageView];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.contentView.bounds.size.width;
CGFloat height = self.contentView.bounds.size.height;
self.customImageView.frame = CGRectMake(10, 10, 40, height - 20);
self.titleLabel.frame = CGRectMake(60, 0, width - 70, height);
}
- (void)prepareForReuse {
[super prepareForReuse];
self.titleLabel.text = nil;
self.customImageView.image = nil;
}
- (void)setShop:(NJShop *)shop {
_shop = shop;
self.titleLabel.text = shop.name;
self.customImageView.image = [UIImage imageNamed:shop.icon];
}
@end
Swift
class CustomTableViewCell: UITableViewCell {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16)
return label
}()
lazy var customImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
var shop: Shop? {
didSet {
titleLabel.text = shop?.name
customImageView.image = UIImage(named: shop?.icon ?? "")
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(titleLabel)
contentView.addSubview(customImageView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let width = contentView.bounds.width
let height = contentView.bounds.height
customImageView.frame = CGRect(x: 10, y: 10, width: 40, height: height - 20)
titleLabel.frame = CGRect(x: 60, y: 0, width: width - 70, height: height)
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = nil
customImageView.image = nil
}
}
手动 frame 布局时代码中提到的"框架模型(Frame Model)"是早期优化手段:将 Cell 内所有子控件的 frame 预计算后缓存到模型中,避免
layoutSubviews重复计算。现代开发使用 Auto Layout 后已不需要这种模式。
六、汽车数据展示案例
通过一个完整案例演示分组列表、右侧索引、模型转换。
数据模型
Objective-C
// Car.h
@interface Car : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *icon;
+ (instancetype)carWithDict:(NSDictionary *)dict;
@end
// Car.m
@implementation Car
+ (instancetype)carWithDict:(NSDictionary *)dict {
Car *car = [[self alloc] init];
car.name = dict[@"name"];
car.icon = dict[@"icon"];
return car;
}
@end
// CarGroup.h
@interface CarGroup : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSArray<Car *> *cars;
+ (instancetype)carGroupWithDict:(NSDictionary *)dict;
@end
// CarGroup.m
@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"]) {
[cars addObject:[Car carWithDict:carDict]];
}
group.cars = cars;
return group;
}
@end
setValuesForKeysWithDictionary:要求字典 key 与属性名完全匹配,且遇到不存在的 key 会崩溃(需重写setValue:forUndefinedKey:)。手动赋值更安全可控。
视图控制器
Objective-C
@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic, copy) NSArray<CarGroup *> *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 clearColor];
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"carCell"];
}
- (NSArray<CarGroup *> *)carGroups {
if (!_carGroups) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"cars" ofType:@"plist"];
NSArray *dictArray = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *groups = [NSMutableArray array];
for (NSDictionary *dict in dictArray) {
[groups addObject:[CarGroup carGroupWithDict:dict]];
}
_carGroups = groups;
}
return _carGroups;
}
#pragma mark - DataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.carGroups.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.carGroups[section].cars.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"carCell" forIndexPath:indexPath];
Car *car = self.carGroups[indexPath.section].cars[indexPath.row];
cell.textLabel.text = car.name;
cell.imageView.image = [UIImage imageNamed:car.icon];
return cell;
}
#pragma mark - Delegate
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.carGroups[section].title;
}
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
// KVC 收集所有 group 的 title
return [self.carGroups valueForKeyPath:@"title"];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES]; // 点击后取消选中
Car *car = self.carGroups[indexPath.section].cars[indexPath.row];
NSLog(@"选中了: %@", car.name);
}
@end
Swift
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var carGroups: [CarGroup] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.sectionIndexColor = .red
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "carCell")
loadData()
}
private func loadData() {
let path = Bundle.main.path(forResource: "cars", ofType: "plist")!
let dictArray = NSArray(contentsOfFile: path) as! [[String: Any]]
carGroups = dictArray.map { CarGroup(dict: $0) }
}
func numberOfSections(in tableView: UITableView) -> Int { carGroups.count }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
carGroups[section].cars.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "carCell", for: indexPath)
let car = carGroups[indexPath.section].cars[indexPath.row]
cell.textLabel?.text = car.name
cell.imageView?.image = UIImage(named: car.icon)
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
carGroups[section].title
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
carGroups.map { $0.title }
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
七、Cell 不等高
方法一:heightForRowAtIndexPath(固定/简单计算)
实现 tableView:heightForRowAtIndexPath: 代理方法,每行返回不同高度:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row % 2 == 0) {
return 60.0;
} else {
return 100.0;
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
indexPath.row % 2 == 0 ? 60 : 100
}
适用于高度规则简单、可直接判断的场景。
方法二:Auto Layout 自动计算(iOS 8+)
iOS 8 起支持根据约束自动计算 Cell 高度,是现代开发的首选。
配置 TableView
self.tableView.estimatedRowHeight = 80.0; // 估算高度,接近实际平均值即可
self.tableView.rowHeight = UITableViewAutomaticDimension; // 自动计算
tableView.estimatedRowHeight = 80
tableView.rowHeight = .automaticDimension
Cell 约束要求
子视图必须从 contentView 顶部到底部形成完整的约束链,系统才能计算出高度:
// CustomTableViewCell.m
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.numberOfLines = 0; // 多行
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.titleLabel];
self.detailLabel = [[UILabel alloc] init];
self.detailLabel.numberOfLines = 0;
self.detailLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:self.detailLabel];
[NSLayoutConstraint activateConstraints:@[
// titleLabel:顶部、左右
[self.titleLabel.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:10],
[self.titleLabel.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:15],
[self.titleLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-15],
// detailLabel:在 titleLabel 下方,左右,底部闭合
[self.detailLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:8],
[self.detailLabel.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:15],
[self.detailLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-15],
[self.detailLabel.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-10]
]];
}
return self;
}
关键:底部必须有一个视图约束到
contentView.bottomAnchor,形成完整的垂直约束链,否则系统无法计算高度。
方法三:预计算高度并缓存(高性能方案)
当高度计算复杂(如大量文本、动态内容)时,在 heightForRowAtIndexPath: 中实时计算会导致滚动卡顿。应提前计算并缓存。
最佳实践:估算 + 精确缓存
@interface ViewController ()
@property (nonatomic, strong) NSMutableDictionary<NSIndexPath *, NSNumber *> *heightCache;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.heightCache = [NSMutableDictionary dictionary];
self.tableView.estimatedRowHeight = 80.0; // 估算高度,用于滚动条计算
}
// 返回估算高度(快速)
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *cached = self.heightCache[indexPath];
return cached ? cached.floatValue : 80.0;
}
// 返回精确高度(计算后缓存)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *cached = self.heightCache[indexPath];
if (cached) {
return cached.floatValue;
}
CGFloat height = [self calculateHeightForIndexPath:indexPath];
self.heightCache[indexPath] = @(height);
return height;
}
- (CGFloat)calculateHeightForIndexPath:(NSIndexPath *)indexPath {
NSString *text = self.dataArray[indexPath.row];
CGSize maxSize = CGSizeMake([UIScreen mainScreen].bounds.size.width - 30, CGFLOAT_MAX);
CGRect rect = [text boundingRectWithSize:maxSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:17]}
context:nil];
return rect.size.height + 20; // 上下各 10pt 内边距
}
多 section 时用
NSIndexPath作为缓存 key(不要只用row),因为不同 section 的相同 row 是不同的行。
三种方案对比
| 方案 | 实现难度 | 性能 | 适用场景 |
|---|---|---|---|
heightForRowAtIndexPath: |
低 | 高(固定值) | 高度规则简单 |
| Auto Layout 自动计算 | 中 | 中(系统计算) | 内容动态、约束清晰 |
| 预计算 + 缓存 | 高 | 最高 | 复杂文本、高性能列表 |
八、总结
UITableView通过dataSource获取数据,通过delegate处理交互和布局。- Cell 重用是高性能的核心:只创建屏幕可见的 Cell,滚动时复用。务必注册 Cell 并使用
dequeueReusableCellWithIdentifier:forIndexPath:。 - 自定义 Cell 时子视图添加到
contentView,重写prepareForReuse重置状态。 - 不等高 Cell 优先使用 Auto Layout 自动计算(
UITableViewAutomaticDimension+estimatedRowHeight),复杂场景用预计算缓存方案。 setValuesForKeysWithDictionary:方便但不安全,推荐手动赋值或使用YYModel/Codable。

浙公网安备 33010602011771号