UITableView — 是 UIKit 中用于在 iOS 中显示垂直列表的主要组件。表格的每一行都是一个 UITableViewCell 单元格,具有预定义的样式。我们解释 UITableView 的基础知识:DataSource、委托、单元格重用以及 iOS 应用中表格的性能。根据苹果公司(Human Interface Guidelines,2026)的数据,UITableView 仍然是 iOS 中最常用的 UI 组件 — App Store 前 100 名应用中 85% 包含至少一个表格。对于复杂布局,请阅读 关于 UICollectionView 的文章。
要点
UITableView — 是 UIKit 框架中的一个类,用于在单列中显示可滚动的行列表。每一行由一个 UITableViewCell 对象表示。UITableView 出现在 iPhone OS 2(2008)中,从那时起一直是 iOS 中列表的主要组件。UITableView 的架构基于 MVC 模式:数据由 DataSource 管理,外观和行为由 Delegate 管理,而视图由表格本身管理。
两种表格样式:.plain(带有可选部分的连续列表)和 .grouped(带有圆角和边距的分组部分)。从 iOS 13 开始添加了 .insetGrouped — 带有边缘边距的分组样式,用于设置和健康应用程序。样式选择会影响默认外观:纯表格在滚动时将部分页眉固定在顶部(粘性页眉),分组则不会。
从 iOS 8 开始,UITableView 支持自动调整大小的单元格(self-sizing cells)。要启用,设置 tableView.estimatedRowHeight(例如 80)和 tableView.rowHeight = UITableView.automaticDimension。表格根据单元格内的 Auto Layout 约束自动计算行高。Self-sizing 会降低复杂单元格的性能 — 对于包含 500 多个元素的表格使用固定高度(rowHeight)。
UITableViewCell 提供四种内置样式,涵盖 80% 的场景,无需创建自定义单元格。每种样式由 textLabel、detailTextLabel 和 imageView 的组合组成。样式选择在单元格初始化时通过 init(style:reuseIdentifier:) 设置。
| 样式 | textLabel | detailTextLabel | imageView | 示例 |
|---|---|---|---|---|
| .default (.basic) | 左对齐,粗体 | 无 | 可选 | 菜单,项目列表 |
| .subtitle | 左对齐,粗体 | 在 textLabel 下方,灰色 | 可选 | 联系人,播放列表 |
| .value1 | 左对齐,粗体 | 右对齐,灰色 | 无 | 带值的设置 |
| .value2 | 右对齐,蓝色 | 左对齐,灰色 | 无 | 电话簿(iOS 6 样式) |
// 使用自定义单元格创建表格
class ContactListController: UIViewController {
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = 60
view.addSubview(tableView)
// Auto Layout
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
extension ContactListController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
contacts.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let contact = contacts[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = contact.name
content.secondaryText = contact.phone
content.image = UIImage(systemName: "person.circle")
cell.contentConfiguration = content
return cell
}
}
UIContentConfiguration(iOS 14+)— 通过 contentConfiguration 配置单元格的现代方式,而不是直接访问 textLabel/detailTextLabel。使用 cell.defaultContentConfiguration() 或创建自定义 UIContentConfiguration。优点:开箱即用支持动态类型、深色主题和 VoiceOver。苹果(WWDC 2024)推荐所有新的 UITableView 使用 contentConfiguration。
UITableViewDataSource — 为表格提供数据的协议。必需方法:tableView(_:numberOfRowsInSection:) 和 tableView(_:cellForRowAt:)。没有它们,表格无法显示任何行。UITableViewDelegate — 用于管理外观和行为的协议:行高、选择、编辑、上下文操作。
// 多个部分和编辑的示例
extension ContactListController: UITableViewDelegate {
// 每行的自定义高度
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// 处理选择
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// 滑动删除
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "删除") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Delegate 性能:尺寸方法(heightForRowAt)在每次滚动时针对每个可见行调用。对于统一高度,直接设置 rowHeight — 这比 heightForRowAt 快 10 倍。对于自调整大小单元格,设置 estimatedRowHeight 和 automaticDimension — 表格将仅为可见行调用 heightForRowAt,其余行使用 estimatedRowHeight。
重用队列 — 可重用 UITableView 单元格的池。当单元格离开屏幕时,不会被删除,而是放入队列中。新单元格不会从头创建 — 系统调用 dequeueReusableCell(withIdentifier:for:),它从队列返回一个单元格,如果队列为空则创建一个新单元格。这是 UITableView 性能的关键机制。
// 带缓存的自定义单元格
class ContactCell: UITableViewCell {
private let avatarView = UIImageView()
private let nameLabel = UILabel()
private let subtitleLabel = UILabel()
override init(style: UITableViewCell.Style, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(avatarView)
contentView.addSubview(nameLabel)
contentView.addSubview(subtitleLabel)
// 配置 Auto Layout 约束
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... 约束 ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// 异步加载头像
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// 注册和使用
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// 在 cellForRowAt:中
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — 在单元格放入重用队列之前调用的方法。重置单元格的所有状态:文本、图像、动画、加载。没有 prepareForReuse,重用单元格可能会显示旧数据:之前的图像在新的加载完成之前会显示一瞬间。苹果(WWDC 2023)建议通过 prepareForReuse 中的 URLSessionTask.cancel() 清除异步操作。
部分 在 UITableView 中对逻辑相关的行进行分组。每个部分可以有 header(页眉)和 footer(页脚)。部分数量由 numberOfSections(in:) 方法确定(默认为 1)。对于 grouped 和 insetGrouped 样式,部分通过边距和圆角在视觉上分离。
| DataSource 方法 | 描述 | 返回值 |
|---|---|---|
| numberOfSections | 部分数量(默认为 1) | Int |
| numberOfRowsInSection | 部分中的行数 | Int |
| titleForHeaderInSection | 部分页眉的文本 | String? |
| titleForFooterInSection | 部分页脚的文本 | String? |
| viewForHeaderInSection | 页眉的自定义视图 | UIView? |
| heightForHeaderInSection | 页眉高度 | CGFloat |
索引:为快速在部分之间导航添加索引 — 显示在右侧的字符串数组。实现 sectionIndexTitles(for:) — 返回每个部分首字母的数组。对于 26 个以上的部分,这极大地改善了用户体验。在 iOS 15+ 中,表格支持 sectionHeaderTopPadding — 状态栏和第一个页眉之间的间距,可以重置为 0 以实现紧凑布局。
性能 UITableView 对于包含长列表的应用程序至关重要。主要问题:由于 cellForRowAt 中的繁重操作导致的滚动卡顿(jank)、heightForRowAt 的频繁调用、缺少预取。苹果(WWDC 2025)强调了针对 1000 多个元素的表格的五项关键实践。
// 使用预取进行优化
extension ContactListController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.prefetch(url: contact.avatarUrl)
}
}
func tableView(_ tableView: UITableView,
cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.cancelPrefetch(url: contact.avatarUrl)
}
}
}
// 使用 DiffableDataSource 实现流畅更新
class ModernTableController: UIViewController {
private var dataSource: UITableViewDiffableDataSource<Section, Contact>!
func applyContacts(_ contacts: [Contact]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Contact>()
snapshot.appendSections([.main])
snapshot.appendItems(contacts)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
实际结果:应用这些优化可在 iPhone 12 及更旧设备上将滚动 FPS 从 30 提高到 60(根据苹果测试,2025)。在 IT Sectr 项目中,我们对所有新表格使用 DiffableDataSource,并为包含图像(头像、预览、应用图标)的表格添加预取。
常见问题
检查:(1)contentSize > frame.size — 表格的内容必须大于其高度;(2)表格不在另一个拦截手势的 UIScrollView 内部;(3)isScrollEnabled = true;(4)numberOfRowsInSection 返回正确的数字。典型问题 — 表格使用 heightForRowAt = UITableView.automaticDimension 而没有 estimatedRowHeight,导致无限计算高度。
使用 performBatchUpdates 或 DiffableDataSource 而不是 reloadData()。reloadData() 会导致无动画的完全重新加载,造成视觉闪烁。对于点更新,使用:reloadRows(at:with:)、insertRows、deleteRows。performBatchUpdates 内的所有更改会同时动画。DiffableDataSource 自动执行此操作,无需手动调用。
UITableView — 单列垂直列表,具有预定义的单元格样式和内置编辑。UICollectionView — 灵活的布局(网格、列表、轮播、瀑布流),元素可任意放置。对于简单列表(设置、联系人、消息)选择 UITableView。对于画廊、目录、面板和任何非线性布局选择 UICollectionView。
设置 tableView.tableFooterView = UIView(frame: .zero)。默认情况下,UITableView 在最后一个元素下方显示空白行(分隔符)直到底部边缘。在 tableFooterView 中设置空的 UIView 可以移除它们。对于分组样式,这不是必需的 — 表格只显示实际行,最后一节带有圆角。
设置 tableView.rowHeight = 80 和 tableView.estimatedRowHeight = 0 或省略 estimatedRowHeight。使用固定高度时,UITableView 不会调用 heightForRowAt,从而使包含 100 多个元素的列表的渲染速度提高 5–10 倍。对于自定义高度的单元格(不同内容),使用 self-sizing:estimatedRowHeight + UITableView.automaticDimension。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。