UITableView: Basics, Lists and Cells in iOS Apps

Author: IT Sectr Published: 2026-02-23 Reading time: 9 min

UITableView — the main UIKit component for displaying vertical lists in iOS. Each table row is a UITableViewCell with predefined styles. We explain the basics of UITableView: DataSource, delegate, cell reuse, and table performance in iOS applications. According to Apple (Human Interface Guidelines, 2026), UITableView remains the most used UI component in iOS — 85% of apps in the App Store top 100 contain at least one table. For complex layouts, read the article about UICollectionView.

Key Takeaways

  • UITableView — a UIKit component for vertical lists, available since iOS 2.0 (iPhone OS 2).
  • UITableViewCell — a table cell with four built-in styles and customization options.
  • UITableViewDataSource — a protocol that defines the number of rows and cells for each section.
  • UITableViewDelegate — a protocol for handling selection, editing, and row height configuration.
  • UITableViewDiffableDataSource — a modern DataSource (iOS 13+) with automatic change calculation.

What is UITableView in iOS?

UITableView is a class from the UIKit framework designed to display a scrollable list of rows in a single column. Each row is represented by a UITableViewCell object. UITableView first appeared in iPhone OS 2 (2008) and has remained the primary component for lists in iOS ever since. The UITableView architecture is built on the MVC pattern: data is managed by the DataSource, appearance and behavior by the Delegate, and the presentation by the table itself.

Two table styles: .plain (continuous list with optional sections) and .grouped (grouped sections with rounded corners and margins). iOS 13 added .insetGrouped — a grouped style with edge margins, used in Settings and Health apps. The choice of style affects the default appearance: plain tables attach section headers to the top when scrolling (sticky header), grouped tables do not.

Self-Sizing Cells

Starting with iOS 8, UITableView supports self-sizing cells. To enable, set tableView.estimatedRowHeight (e.g., 80) and tableView.rowHeight = UITableView.automaticDimension. The table automatically calculates row height based on Auto Layout constraints inside the cell. Self-sizing reduces performance for complex cells — use fixed height (rowHeight) for tables with 500+ items.

UITableViewCell Styles: Basic, Subtitle, Value1, Value2

UITableViewCell provides four built-in styles that cover 80% of scenarios without needing to create a custom cell. Each style consists of a combination of textLabel, detailTextLabel, and imageView. The style is chosen when initializing the cell via init(style:reuseIdentifier:).

StyletextLabeldetailTextLabelimageViewExample
.default (.basic)Left, boldNoneOptionalMenu, list of items
.subtitleLeft, boldBelow textLabel, grayOptionalContacts, playlists
.value1Left, boldRight, grayNoneSettings with values
.value2Right, blueLeft, grayNonePhone book (iOS 6-style)
swift
// Creating a table with custom cells
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+) — a modern way to configure cells via contentConfiguration instead of directly accessing textLabel/detailTextLabel. Use cell.defaultContentConfiguration() or create a custom UIContentConfiguration. Advantages: built-in support for Dynamic Type, dark mode, and VoiceOver. Apple (WWDC 2024) recommends contentConfiguration for all new UITableViews.

DataSource and Delegate: Required Methods

UITableViewDataSource — a protocol that provides data for the table. Required methods: tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAt:). Without them, the table cannot display a single row. UITableViewDelegate — a protocol for managing appearance and behavior: row height, selection, editing, contextual actions.

swift
// Example with multiple sections and editing
extension ContactListController: UITableViewDelegate {

    // Custom height for each row
    func tableView(_ tableView: UITableView,
                         heightForRowAt indexPath: IndexPath) -> CGFloat {
        80
    }

    // Selection handling
    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: "Delete") {
            [weak self] _, _, completion in
            self?.contacts.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
            completion(true)
        }
        return UISwipeActionsConfiguration(actions: [deleteAction])
    }
}

Delegate Performance: sizing methods (heightForRowAt) are called for every visible row on each scroll. For uniform height, set rowHeight directly — this is 10x faster than heightForRowAt. For self-sizing cells, set estimatedRowHeight and automaticDimension — the table calls heightForRowAt only for visible rows and uses estimatedRowHeight for the rest.

Cell Reuse: Reuse Queue and Register

Reuse queue — a pool of reusable UITableView cells. When a cell scrolls off screen, it is not deleted but placed in the queue. New cells are not created from scratch — the system calls dequeueReusableCell(withIdentifier:for:), which returns a cell from the queue or creates a new one if the queue is empty. This is the key performance mechanism of UITableView.

swift
// Custom cell with caching
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)

        // Configuring Auto Layout constraints
        avatarView.translatesAutoresizingMaskIntoConstraints = false
        // ... constraints ...
    }

    func configure(with contact: Contact) {
        nameLabel.text = contact.name
        subtitleLabel.text = contact.phone
        // Async avatar loading
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        avatarView.image = nil
        nameLabel.text = nil
        subtitleLabel.text = nil
    }
}

// Registration and usage
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")

// In cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell

prepareForReuse() — a method called before placing a cell into the reuse queue. Reset all cell state: text, images, animations, loading operations. Without prepareForReuse, a reused cell may show old data: the previous image will be visible for a fraction of a second until the new one loads. Apple (WWDC 2023) recommends canceling asynchronous operations via URLSessionTask.cancel() in prepareForReuse.

Sections, Header and Footer: Grouping Data

Sections in UITableView group logically related rows. Each section can have a header and a footer. The number of sections is determined by the numberOfSections(in:) method (default is 1). For grouped and insetGrouped styles, sections are visually separated by margins and rounded corners.

DataSource MethodDescriptionReturn Value
numberOfSectionsNumber of sections (default 1)Int
numberOfRowsInSectionNumber of rows in a sectionInt
titleForHeaderInSectionSection header title textString?
titleForFooterInSectionSection footer title textString?
viewForHeaderInSectionCustom view for headerUIView?
heightForHeaderInSectionHeader heightCGFloat

Indexing: for quick navigation between sections, add an index title — an array of strings displayed on the right side. Implement sectionIndexTitles(for:) — returns an array of the first letters of each section. For 26+ sections, this critically improves UX. In iOS 15+, tables support sectionHeaderTopPadding — the padding between the status bar and the first header, which can be reset to 0 for a dense layout.

UITableView Performance: Best Practices

Performance of UITableView is critical for apps with long lists. Common issues: slow scrolling (jank) due to heavy operations in cellForRowAt, frequent heightForRowAt calls, lack of prefetching. Apple (WWDC 2025) highlights five key practices for tables with 1000+ items.

  • Flat-height — use constant rowHeight instead of heightForRowAt for all cells. Difference: 60 FPS vs 30 FPS for a table of 500 rows.
  • Prefetching — implement UITableViewDataSourcePrefetching for async data loading (images, API) before the cell appears on screen.
  • Fewer subviews — each subview in contentView increases render time. Use draw() for simple graphics (separators, icons) instead of UIImageView.
  • Heavy operations in background — date formatting, localization, calculations should be done in the model, not in cellForRowAt. Use NSCache for results.
  • DiffableDataSource — replace reloadData() with apply(snapshot) — animation happens for free, the table does not reload entirely.
swift
// Optimization with 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)
        }
    }
}

// Using DiffableDataSource for smooth updates
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)
    }
}

Practical result: applying these optimizations increases scroll FPS from 30 to 60 on iPhone 12 and newer devices (according to Apple test data, 2025). In IT Sectr projects, we use DiffableDataSource for all new tables and add prefetching for tables with images (avatars, previews, app icons).

Frequently Asked Questions

UITableView is not scrolling — what to do?

Check: (1) contentSize > frame.size — the table must have more content than its height; (2) the table is not inside another UIScrollView that intercepts gestures; (3) isScrollEnabled = true; (4) numberOfRowsInSection returns the correct count. A typical problem is a table with heightForRowAt = UITableView.automaticDimension without estimatedRowHeight, which causes infinite height calculation.

How to update UITableView without blinking?

Use performBatchUpdates or DiffableDataSource instead of reloadData(). reloadData() causes a full reload without animation, creating visual blinking. For targeted updates use: reloadRows(at:with:), insertRows, deleteRows. All changes inside performBatchUpdates animate simultaneously. DiffableDataSource does this automatically without manual calls.

What is the difference between UITableView and UICollectionView?

UITableView is a single-column vertical list with predefined cell styles and built-in editing. UICollectionView is a flexible layout (grid, list, carousel, waterfall) with arbitrary element positioning. Choose UITableView for simple lists (settings, contacts, messages). Choose UICollectionView for galleries, catalogs, boards, and any non-linear layouts.

How to remove empty rows below content in UITableView?

Set tableView.tableFooterView = UIView(frame: .zero). By default, UITableView displays empty rows (separators) below the last item to the bottom edge. Setting an empty UIView as tableFooterView removes them. For grouped style this is not required — the table only displays actual rows with rounded corners on the last section.

How to make UITableView with constant height?

Set tableView.rowHeight = 80 and tableView.estimatedRowHeight = 0 or omit estimatedRowHeight. With fixed height, UITableView does not call heightForRowAt, which speeds up rendering by 5–10x for lists of 100+ items. For cells with custom height (different content), use self-sizing: estimatedRowHeight + UITableView.automaticDimension.

Summary

  • UITableView — the main UIKit component for vertical lists with DataSource and Delegate architecture.
  • UITableViewCell — 4 built-in styles (.default, .subtitle, .value1, .value2) and custom ones via contentConfiguration.
  • Reuse queue — a pool of reusable cells; register + dequeueReusableCell + prepareForReuse are mandatory.
  • DiffableDataSource (iOS 13+) — a type-safe DataSource with automatic change calculation and animation.
  • Performance: fixed rowHeight, prefetching, fewer subviews, heavy operations in background.
  • 85% of iOS apps in the App Store top 100 use UITableView for displaying content.
  • For grids, carousels, and complex layouts, use UICollectionView instead of UITableView.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also