UITableView: kiến thức cơ bản, danh sách và ô trong ứng dụng iOS

Tác giả: IT Sectr Đã đăng: 2026-02-23 Thời gian đọc: 9 phút

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 — thành phần UIKit cho danh sách dọc, khả dụng từ iOS 2.0 (iPhone OS 2).
  • UITableViewCell — ô bảng với bốn kiểu tích hợp và tùy chọn tùy chỉnh.
  • UITableViewDataSource — giao thức xác định số lượng hàng và ô cho mỗi phần.
  • UITableViewDelegate — giao thức xử lý lựa chọn, chỉnh sửa và cấu hình chiều cao hàng.
  • UITableViewDiffableDataSource — DataSource hiện đại (iOS 13+) với tính toán thay đổi tự động.

UITableView trong iOS là gì?

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.

Ô tự điều chỉnh kích thước

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ử.

Kiểu UITableViewCell: Basic, Subtitle, Value1, Value2

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ểutextLabeldetailTextLabelimageViewVí dụ
.default (.basic)Trái, đậmKhông cóTùy chọnMenu, danh sách mục
.subtitleTrái, đậmDưới textLabel, xámTùy chọnDanh bạ, danh sách phát
.value1Trái, đậmPhải, xámKhông cóCài đặt có giá trị
.value2Phải, xanhTrái, xámKhông cóSổ điện thoại (kiểu iOS 6)
swift
// 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.

DataSource và Delegate: phương thức bắt buộc

UITableViewDataSource — giao thức cung cấp dữ liệu cho bảng. Phương thức bắt buộc: tableView(_:numberOfRowsInSection:)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.

swift
// 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.

Tái sử dụng ô: hàng đợi tái sử dụng và đăng ký

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.

swift
// Ô 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, tiêu đề và chân trang: nhóm dữ liệu

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 DataSourceMô tảGiá trị trả về
numberOfSectionsSố lượng phần (mặc định 1)Int
numberOfRowsInSectionSố lượng hàng trong một phầnInt
titleForHeaderInSectionVăn bản tiêu đề của phầnString?
titleForFooterInSectionVăn bản chân trang của phầnString?
viewForHeaderInSectionChế độ xem tùy chỉnh cho tiêu đềUIView?
heightForHeaderInSectionChiề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 UITableView: thực hành tốt nhất

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ử.

  • Chiều cao cố định — sử dụng rowHeight hằng số thay vì heightForRowAt cho tất cả ô. Khác biệt: 60 FPS so với 30 FPS cho bảng 500 hàng.
  • Prefetching — triển khai UITableViewDataSourcePrefetching để tải dữ liệu bất đồng bộ (hình ảnh, API) trước khi ô xuất hiện trên màn hình.
  • Ít chế độ xem con hơn — mỗi chế độ xem con trong contentView làm tăng thời gian kết xuất. Sử dụng draw() cho đồ họa đơn giản (dấu phân cách, biểu tượng) thay vì UIImageView.
  • Thao tác nặng trong nền — định dạng ngày tháng, bản địa hóa, tính toán nên được thực hiện trong mô hình, không phải trong cellForRowAt. Sử dụng NSCache cho kết quả.
  • DiffableDataSource — thay thế reloadData() bằng apply(snapshot) — hoạt ảnh diễn ra miễn phí, bảng không bị tải lại hoàn toàn.
swift
// 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

UITableView không cuộn được — phải làm sao?

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.

Làm thế nào để cập nhật UITableView không bị nhấp nháy?

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.

Sự khác biệt giữa UITableView và UICollectionView là gì?

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.

Làm thế nào để loại bỏ các hàng trống dưới nội dung trong UITableView?

Đặ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.

Làm thế nào để tạo UITableView với chiều cao cố định?

Đặt tableView.rowHeight = 80tableView.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

  • UITableView — thành phần UIKit chính cho danh sách dọc với kiến trúc DataSource và Delegate.
  • UITableViewCell — 4 kiểu tích hợp (.default, .subtitle, .value1, .value2) và tùy chỉnh qua contentConfiguration.
  • Hàng đợi tái sử dụng — một nhóm ô có thể tái sử dụng; register + dequeueReusableCell + prepareForReuse là bắt buộc.
  • DiffableDataSource (iOS 13+) — DataSource type-safe với tính toán thay đổi tự động và hoạt ảnh.
  • Hiệu suất: rowHeight cố định, prefetching, ít chế độ xem con, thao tác nặng trong nền.
  • 85% ứng dụng iOS trong top 100 App Store sử dụng UITableView để hiển thị nội dung.
  • Đối với lưới, băng chuyền và bố cục phức tạp, hãy sử dụng UICollectionView thay vì UITableView.

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.

Thảo luận dự án

Đọc thêm