Introduction
UITableView is the most commonly used component in iOS for displaying scrollable lists of data. It requires a data source (providing cells and section info) and optionally a delegate (handling selection and layout). This article shows a complete, working example of a UITableView in Swift — from basic setup through custom cells, sections, swipe actions, and the modern diffable data source API.
Basic UITableView Setup
1import UIKit
2
3class FruitListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
4
5 let tableView = UITableView()
6 let fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
7
8 override func viewDidLoad() {
9 super.viewDidLoad()
10 title = "Fruits"
11
12 tableView.dataSource = self
13 tableView.delegate = self
14 tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
15 tableView.frame = view.bounds
16 tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
17 view.addSubview(tableView)
18 }
19
20 // MARK: - UITableViewDataSource
21
22 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
23 return fruits.count
24 }
25
26 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
27 let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
28 cell.textLabel?.text = fruits[indexPath.row]
29 cell.accessoryType = .disclosureIndicator
30 return cell
31 }
32
33 // MARK: - UITableViewDelegate
34
35 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
36 tableView.deselectRow(at: indexPath, animated: true)
37 print("Selected: \(fruits[indexPath.row])")
38 }
39}
This is the minimum setup: register a cell, implement two data source methods, and handle selection.
Custom UITableViewCell
1class FruitCell: UITableViewCell {
2 static let reuseIdentifier = "FruitCell"
3
4 let fruitImageView = UIImageView()
5 let nameLabel = UILabel()
6 let detailLabel = UILabel()
7
8 override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
9 super.init(style: style, reuseIdentifier: reuseIdentifier)
10 setupUI()
11 }
12
13 required init?(coder: NSCoder) {
14 fatalError("init(coder:) has not been implemented")
15 }
16
17 private func setupUI() {
18 fruitImageView.translatesAutoresizingMaskIntoConstraints = false
19 fruitImageView.contentMode = .scaleAspectFill
20 fruitImageView.layer.cornerRadius = 20
21 fruitImageView.clipsToBounds = true
22
23 nameLabel.translatesAutoresizingMaskIntoConstraints = false
24 nameLabel.font = .systemFont(ofSize: 17, weight: .semibold)
25
26 detailLabel.translatesAutoresizingMaskIntoConstraints = false
27 detailLabel.font = .systemFont(ofSize: 14)
28 detailLabel.textColor = .secondaryLabel
29
30 contentView.addSubview(fruitImageView)
31 contentView.addSubview(nameLabel)
32 contentView.addSubview(detailLabel)
33
34 NSLayoutConstraint.activate([
35 fruitImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
36 fruitImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
37 fruitImageView.widthAnchor.constraint(equalToConstant: 40),
38 fruitImageView.heightAnchor.constraint(equalToConstant: 40),
39
40 nameLabel.leadingAnchor.constraint(equalTo: fruitImageView.trailingAnchor, constant: 12),
41 nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
42 nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
43
44 detailLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
45 detailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 2),
46 detailLabel.trailingAnchor.constraint(equalTo: nameLabel.trailingAnchor),
47 detailLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
48 ])
49 }
50
51 func configure(name: String, detail: String) {
52 nameLabel.text = name
53 detailLabel.text = detail
54 fruitImageView.image = UIImage(systemName: "leaf.fill")
55 }
56}
Register and use in the view controller:
1tableView.register(FruitCell.self, forCellReuseIdentifier: FruitCell.reuseIdentifier)
2
3func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
4 let cell = tableView.dequeueReusableCell(withIdentifier: FruitCell.reuseIdentifier, for: indexPath) as! FruitCell
5 cell.configure(name: fruits[indexPath.row], detail: "Fresh fruit")
6 return cell
7}
Sections
1let sections = ["Fruits", "Vegetables"]
2let data = [
3 ["Apple", "Banana", "Cherry"],
4 ["Carrot", "Broccoli", "Spinach"]
5]
6
7func numberOfSections(in tableView: UITableView) -> Int {
8 return sections.count
9}
10
11func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
12 return sections[section]
13}
14
15func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
16 return data[section].count
17}
18
19func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
20 let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
21 cell.textLabel?.text = data[indexPath.section][indexPath.row]
22 return cell
23}
Swipe Actions
1func tableView(_ tableView: UITableView,
2 trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
3 let delete = UIContextualAction(style: .destructive, title: "Delete") { _, _, completion in
4 self.fruits.remove(at: indexPath.row)
5 tableView.deleteRows(at: [indexPath], with: .automatic)
6 completion(true)
7 }
8
9 let edit = UIContextualAction(style: .normal, title: "Edit") { _, _, completion in
10 print("Edit \(self.fruits[indexPath.row])")
11 completion(true)
12 }
13 edit.backgroundColor = .systemBlue
14
15 return UISwipeActionsConfiguration(actions: [delete, edit])
16}
Pull to Refresh
1override func viewDidLoad() {
2 super.viewDidLoad()
3
4 let refreshControl = UIRefreshControl()
5 refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
6 tableView.refreshControl = refreshControl
7}
8
9@objc func refresh() {
10 // Reload data from server...
11 DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
12 self.tableView.refreshControl?.endRefreshing()
13 self.tableView.reloadData()
14 }
15}
Common Pitfalls
Forgetting to register the cell: Calling dequeueReusableCell(withIdentifier:for:) without registering the cell class or nib first causes a crash. Always call tableView.register(CellClass.self, forCellReuseIdentifier:) in viewDidLoad.
Adding subviews to cell instead of cell.contentView: UI elements must be added to contentView, not the cell directly. Adding to the cell causes layout issues with editing mode, swipe actions, and accessory views.
Not calling dequeueReusableCell: Creating a new cell every time instead of dequeuing wastes memory and causes scroll performance issues. Always dequeue and configure the returned cell.
Forgetting reloadData after data changes: Modifying the data array without calling tableView.reloadData() (or insertRows/deleteRows) leaves the table view out of sync, causing crashes on scroll.
Cell reuse not resetting state: Dequeued cells retain state from their previous use. If a cell has an image and the new data has no image, the old image persists. Always reset all UI elements in cellForRowAt or override prepareForReuse().
Summary
Implement UITableViewDataSource for numberOfRowsInSection and cellForRowAt (minimum required)
Implement UITableViewDelegate for didSelectRowAt and layout customization
Always register cell classes before dequeuing — use tableView.register()
Create custom UITableViewCell subclasses for complex layouts with Auto Layout
Add subviews to contentView, not the cell directly
Use swipe actions, pull-to-refresh, and sections to build feature-rich table views