UITableView — thành phần UIKit chính để hiển thị danh sách dọc trong iOS. Mỗi hàng của bảng là một ô UITableViewCell với các kiểu được xác định trước. Chúng tôi giải thích kiến thức cơ bản về UITableView: DataSource, delegate, tái sử dụng ô và hiệu suất bảng trong ứng dụng iOS. Theo Apple (Nguyên tắc Giao diện Người dùng, 2026), UITableView vẫn là thành phần UI được sử dụng nhiều nhất trong iOS — 85% ứng dụng trong top 100 App Store chứa ít nhất một bảng. Đối với bố cục phức tạp, hãy đọc bài viết về UICollectionView.
Điểm chính
UITableView là một lớp từ framework UIKit được thiết kế để hiển thị danh sách các hàng có thể cuộn trong một cột duy nhất. Mỗi hàng được đại diện bởi một đối tượng UITableViewCell. UITableView xuất hiện lần đầu trong iPhone OS 2 (2008) và từ đó vẫn là thành phần chính cho danh sách trong iOS. Kiến trúc của UITableView dựa trên mô hình MVC: dữ liệu được quản lý bởi DataSource, giao diện và hành vi bởi Delegate, và trình bày bởi chính bảng.
Hai kiểu bảng: .plain (danh sách liên tục với các phần tùy chọn) và .grouped (các phần được nhóm với góc bo tròn và lề). iOS 13 đã thêm .insetGrouped — kiểu nhóm có lề ở cạnh, được sử dụng trong ứng dụng Cài đặt và Sức khỏe. Việc chọn kiểu ảnh hưởng đến giao diện mặc định: bảng plain gắn tiêu đề phần vào đầu khi cuộn (tiêu đề cố định), bảng grouped thì không.
Bắt đầu từ iOS 8, UITableView hỗ trợ ô tự điều chỉnh kích thước (self-sizing cells). Để kích hoạt, hãy đặt tableView.estimatedRowHeight (ví dụ: 80) và tableView.rowHeight = UITableView.automaticDimension. Bảng tự động tính toán chiều cao hàng dựa trên các ràng buộc Auto Layout bên trong ô. Tự điều chỉnh kích thước làm giảm hiệu suất cho các ô phức tạp — hãy sử dụng chiều cao cố định (rowHeight) cho bảng có 500+ phần tử.
UITableViewCell cung cấp bốn kiểu tích hợp, bao phủ 80% tình huống mà không cần tạo ô tùy chỉnh. Mỗi kiểu bao gồm sự kết hợp của textLabel, detailTextLabel và imageView. Kiểu được chọn khi khởi tạo ô thông qua init(style:reuseIdentifier:).
| Kiểu | textLabel | detailTextLabel | imageView | Ví dụ |
|---|---|---|---|---|
| .default (.basic) | Trái, đậm | Không có | Tùy chọn | Menu, danh sách mục |
| .subtitle | Trái, đậm | Dưới textLabel, xám | Tùy chọn | Danh bạ, danh sách phát |
| .value1 | Trái, đậm | Phải, xám | Không có | Cài đặt có giá trị |
| .value2 | Phải, xanh | Trái, xám | Không có | Sổ điện thoại (kiểu iOS 6) |
// Tạo bảng với ô tùy chỉnh
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+) — cách hiện đại để cấu hình ô thông qua contentConfiguration thay vì truy cập trực tiếp vào textLabel/detailTextLabel. Sử dụng cell.defaultContentConfiguration() hoặc tạo UIContentConfiguration tùy chỉnh. Lợi ích: hỗ trợ tích hợp cho Dynamic Type, chế độ tối và VoiceOver. Apple (WWDC 2024) khuyến nghị contentConfiguration cho tất cả UITableView mới.
UITableViewDataSource — giao thức cung cấp dữ liệu cho bảng. Phương thức bắt buộc: tableView(_:numberOfRowsInSection:) và tableView(_:cellForRowAt:). Nếu không có chúng, bảng không thể hiển thị bất kỳ hàng nào. UITableViewDelegate — giao thức quản lý giao diện và hành vi: chiều cao hàng, lựa chọn, chỉnh sửa, hành động theo ngữ cảnh.
// Ví dụ với nhiều phần và chỉnh sửa
extension ContactListController: UITableViewDelegate {
// Chiều cao tùy chỉnh cho mỗi hàng
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// Xử lý lựa chọn
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// Swipe-to-delete
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Xóa") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Hiệu suất Delegate: các phương thức kích thước (heightForRowAt) được gọi cho mỗi hàng hiển thị ở mỗi lần cuộn. Để có chiều cao đồng nhất, hãy đặt trực tiếp rowHeight — nhanh hơn 10 lần so với heightForRowAt. Đối với ô tự điều chỉnh kích thước, hãy đặt estimatedRowHeight và automaticDimension — bảng chỉ gọi heightForRowAt cho các hàng hiển thị và sử dụng estimatedRowHeight cho phần còn lại.
Hàng đợi tái sử dụng — một nhóm các ô UITableView có thể tái sử dụng. Khi một ô cuộn ra khỏi màn hình, nó không bị xóa mà được đặt vào hàng đợi. Các ô mới không được tạo từ đầu — hệ thống gọi dequeueReusableCell(withIdentifier:for:), trả về một ô từ hàng đợi hoặc tạo ô mới nếu hàng đợi trống. Đây là cơ chế hiệu suất chính của UITableView.
// Ô tùy chỉnh với bộ nhớ đệm
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)
// Cấu hình ràng buộc Auto Layout
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... constraints ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// Tải hình đại diện bất đồng bộ
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// Đăng ký và sử dụng
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// Trong cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — phương thức được gọi trước khi đặt ô vào hàng đợi tái sử dụng. Đặt lại toàn bộ trạng thái ô: văn bản, hình ảnh, hoạt ảnh, hoạt động tải. Nếu không có prepareForReuse, ô được tái sử dụng có thể hiển thị dữ liệu cũ: hình ảnh trước đó sẽ hiển thị trong một phần giây cho đến khi hình ảnh mới được tải. Apple (WWDC 2023) khuyến nghị hủy các hoạt động bất đồng bộ thông qua URLSessionTask.cancel() trong prepareForReuse.
Phần trong UITableView nhóm các hàng có liên quan logic với nhau. Mỗi phần có thể có tiêu đề (header) và chân trang (footer). Số lượng phần được xác định bởi phương thức numberOfSections(in:) (mặc định là 1). Đối với kiểu grouped và insetGrouped, các phần được phân cách trực quan bằng lề và góc bo tròn.
| Phương thức DataSource | Mô tả | Giá trị trả về |
|---|---|---|
| numberOfSections | Số lượng phần (mặc định 1) | Int |
| numberOfRowsInSection | Số lượng hàng trong một phần | Int |
| titleForHeaderInSection | Văn bản tiêu đề của phần | String? |
| titleForFooterInSection | Văn bản chân trang của phần | String? |
| viewForHeaderInSection | Chế độ xem tùy chỉnh cho tiêu đề | UIView? |
| heightForHeaderInSection | Chiều cao tiêu đề | CGFloat |
Đánh chỉ mục: để điều hướng nhanh giữa các phần, hãy thêm tiêu đề chỉ mục — một mảng các chuỗi hiển thị ở bên phải. Triển khai sectionIndexTitles(for:) — trả về một mảng các chữ cái đầu tiên của mỗi phần. Đối với 26+ phần, điều này cải thiện UX đáng kể. Trong iOS 15+, bảng hỗ trợ sectionHeaderTopPadding — khoảng đệm giữa thanh trạng thái và tiêu đề đầu tiên, có thể được đặt lại thành 0 cho bố cục dày đặc.
Hiệu suất của UITableView rất quan trọng đối với các ứng dụng có danh sách dài. Các vấn đề phổ biến: cuộn chậm (jank) do các thao tác nặng trong cellForRowAt, gọi heightForRowAt thường xuyên, thiếu prefetching. Apple (WWDC 2025) nêu bật năm thực hành chính cho bảng có 1000+ phần tử.
// Tối ưu hóa với prefetching
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)
}
}
}
// Sử dụng DiffableDataSource để cập nhật mượt mà
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)
}
}
Kết quả thực tế: áp dụng các tối ưu hóa này giúp tăng FPS cuộn từ 30 lên 60 trên các thiết bị iPhone 12 trở lên (theo dữ liệu thử nghiệm của Apple, 2025). Trong các dự án IT Sectr, chúng tôi sử dụng DiffableDataSource cho tất cả bảng mới và thêm prefetching cho bảng có hình ảnh (hình đại diện, xem trước, biểu tượng ứng dụng).
Câu hỏi thường gặp
Kiểm tra: (1) contentSize > frame.size — bảng phải có nhiều nội dung hơn chiều cao của nó; (2) bảng không nằm trong UIScrollView khác chặn cử chỉ; (3) isScrollEnabled = true; (4) numberOfRowsInSection trả về số lượng chính xác. Một vấn đề điển hình là bảng có heightForRowAt = UITableView.automaticDimension mà không có estimatedRowHeight, dẫn đến tính toán chiều cao vô hạn.
Sử dụng performBatchUpdates hoặc DiffableDataSource thay vì reloadData(). reloadData() gây ra tải lại toàn bộ mà không có hoạt ảnh, tạo ra hiện tượng nhấp nháy. Để cập nhật có mục tiêu, sử dụng: reloadRows(at:with:), insertRows, deleteRows. Tất cả thay đổi trong performBatchUpdates được tạo hoạt ảnh đồng thời. DiffableDataSource thực hiện điều này tự động mà không cần gọi thủ công.
UITableView là danh sách dọc một cột với kiểu ô được xác định trước và chỉnh sửa tích hợp. UICollectionView là bố cục linh hoạt (lưới, danh sách, băng chuyền, thác nước) với vị trí phần tử tùy ý. Chọn UITableView cho danh sách đơn giản (cài đặt, danh bạ, tin nhắn). Chọn UICollectionView cho thư viện ảnh, danh mục, bảng và bất kỳ bố cục phi tuyến tính nào.
Đặt tableView.tableFooterView = UIView(frame: .zero). Theo mặc định, UITableView hiển thị các hàng trống (dấu phân cách) bên dưới mục cuối cùng đến cạnh dưới. Đặt UIView trống làm tableFooterView sẽ loại bỏ chúng. Đối với kiểu grouped, điều này không cần thiết — bảng chỉ hiển thị các hàng thực tế với góc bo tròn ở phần cuối cùng.
Đặt tableView.rowHeight = 80 và tableView.estimatedRowHeight = 0 hoặc bỏ qua estimatedRowHeight. Với chiều cao cố định, UITableView không gọi heightForRowAt, giúp tăng tốc kết xuất 5–10 lần cho danh sách có 100+ mục. Đối với ô có chiều cao tùy chỉnh (nội dung khác nhau), hãy sử dụng tự điều chỉnh kích thước: estimatedRowHeight + UITableView.automaticDimension.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm