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 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.
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 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:).
| Style | textLabel | detailTextLabel | imageView | Example |
|---|---|---|---|---|
| .default (.basic) | Left, bold | None | Optional | Menu, list of items |
| .subtitle | Left, bold | Below textLabel, gray | Optional | Contacts, playlists |
| .value1 | Left, bold | Right, gray | None | Settings with values |
| .value2 | Right, blue | Left, gray | None | Phone book (iOS 6-style) |
// 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.
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.
// 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.
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.
// 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 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 Method | Description | Return Value |
|---|---|---|
| numberOfSections | Number of sections (default 1) | Int |
| numberOfRowsInSection | Number of rows in a section | Int |
| titleForHeaderInSection | Section header title text | String? |
| titleForFooterInSection | Section footer title text | String? |
| viewForHeaderInSection | Custom view for header | UIView? |
| heightForHeaderInSection | Header height | CGFloat |
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.
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.
// 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
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.
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.
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.
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.
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
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.
Read also