Swift
UITableView
iOS Development
Custom Header
Programming

Swift - how to make custom header for UITableView?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

A UITableView section header is not a special magic object. In UIKit it is just a view the table asks you to provide, so the main decision is whether you need a quick static header or a reusable header class with its own layout and behavior. For prototypes, returning a plain UIView is fine. For production screens, a UITableViewHeaderFooterView subclass is usually the cleaner and more scalable option.

Start With the Right Delegate Methods

Per-section headers are created through tableView(_:viewForHeaderInSection:). Their size is usually supplied by tableView(_:heightForHeaderInSection:), or by automatic dimension if you configure the table correctly.

A minimal header can be built inline:

swift
1import UIKit
2
3final class SimpleHeaderViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
4    private let sections = ["Popular", "Recent", "Archived"]
5    private let tableView = UITableView(frame: .zero, style: .plain)
6
7    override func viewDidLoad() {
8        super.viewDidLoad()
9        view.addSubview(tableView)
10        tableView.frame = view.bounds
11        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
12        tableView.dataSource = self
13        tableView.delegate = self
14    }
15
16    func numberOfSections(in tableView: UITableView) -> Int {
17        sections.count
18    }
19
20    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
21        3
22    }
23
24    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
25        UITableViewCell(style: .default, reuseIdentifier: nil)
26    }
27
28    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
29        let header = UIView()
30        header.backgroundColor = .systemGray6
31
32        let label = UILabel(frame: CGRect(x: 16, y: 8, width: tableView.bounds.width - 32, height: 24))
33        label.text = sections[section]
34        label.font = .boldSystemFont(ofSize: 18)
35        header.addSubview(label)
36
37        return header
38    }
39
40    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
41        40
42    }
43}

This pattern works, but it becomes awkward once the header gains icons, buttons, or state.

Prefer UITableViewHeaderFooterView for Real Screens

A reusable header view makes the codebase easier to maintain. It keeps header layout in one place, supports reuse, and avoids rebuilding the whole view hierarchy each time the table scrolls.

swift
1import UIKit
2
3final class SectionHeaderView: UITableViewHeaderFooterView {
4    static let reuseIdentifier = "SectionHeaderView"
5
6    private let titleLabel = UILabel()
7    private let actionButton = UIButton(type: .system)
8    var tapHandler: (() -> Void)?
9
10    override init(reuseIdentifier: String?) {
11        super.init(reuseIdentifier: reuseIdentifier)
12
13        contentView.backgroundColor = .systemBlue
14
15        titleLabel.translatesAutoresizingMaskIntoConstraints = false
16        titleLabel.font = .preferredFont(forTextStyle: .headline)
17        titleLabel.textColor = .white
18
19        actionButton.translatesAutoresizingMaskIntoConstraints = false
20        actionButton.setTitle("Edit", for: .normal)
21        actionButton.tintColor = .white
22        actionButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
23
24        contentView.addSubview(titleLabel)
25        contentView.addSubview(actionButton)
26
27        NSLayoutConstraint.activate([
28            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
29            titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
30
31            actionButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
32            actionButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
33
34            titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: actionButton.leadingAnchor, constant: -12)
35        ])
36    }
37
38    required init?(coder: NSCoder) {
39        fatalError("init(coder:) has not been implemented")
40    }
41
42    func configure(title: String, onTap: @escaping () -> Void) {
43        titleLabel.text = title
44        tapHandler = onTap
45    }
46
47    @objc private func buttonTapped() {
48        tapHandler?()
49    }
50}

Register it once and dequeue it in the delegate:

swift
1tableView.register(SectionHeaderView.self,
2                   forHeaderFooterViewReuseIdentifier: SectionHeaderView.reuseIdentifier)
3
4func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
5    guard let header = tableView.dequeueReusableHeaderFooterView(
6        withIdentifier: SectionHeaderView.reuseIdentifier
7    ) as? SectionHeaderView else {
8        return nil
9    }
10
11    header.configure(title: sections[section]) {
12        print("Tapped section \(section)")
13    }
14    return header
15}
16
17func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
18    52
19}

That pattern is the UIKit equivalent of a reusable cell. The controller decides what data to show. The header manages its own appearance and controls.

Use Auto Layout Instead of Manual Frames

Many header bugs come from using fixed frames after the layout stops being fixed. A label that fits in English may clip in another language. A hard-coded height may fail when Dynamic Type is enabled. Auto Layout is the safer default once the header contains more than one element.

If you want self-sizing headers, enable estimated heights and return UITableView.automaticDimension.

swift
tableView.sectionHeaderHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 52

For this to work well, the header view must have constraints that fully describe its vertical size. In practice that means pinning subviews to the top and bottom of contentView, not only centering them vertically.

Distinguish Section Headers From tableHeaderView

A common source of confusion is mixing section headers with the table's one top banner. tableHeaderView is a single view shown once at the top of the table. viewForHeaderInSection is called for each section. If you want a profile card above the whole list, use tableHeaderView. If you want a label such as “Favorites” above each group of rows, use section headers.

That distinction matters because the APIs and sizing behavior are different. Developers often spend time debugging the wrong header type because the names sound similar.

Keep Interaction Flow Simple

Interactive headers are normal. Filters, expand buttons, sort controls, and disclosure actions all fit well in a header. The important part is keeping the responsibilities separate. The header should not mutate the data source directly. Let it expose a closure or delegate callback, then let the controller update the model and reload the table.

That approach avoids hidden coupling and makes the header reusable in other screens.

Common Pitfalls

  • Building a complex hierarchy inline inside viewForHeaderInSection instead of moving it into a reusable header class.
  • Forgetting to register the UITableViewHeaderFooterView subclass before dequeuing it.
  • Using fixed frames for text that needs Dynamic Type or localization support.
  • Confusing tableHeaderView with per-section headers and debugging the wrong API.
  • Putting data-source mutations inside the header view instead of sending actions back to the controller.

Summary

  • A custom UITableView header is just a view returned by the table delegate.
  • Inline UIView headers are acceptable for simple cases, but reusable header classes scale better.
  • 'UITableViewHeaderFooterView is the preferred UIKit type for maintainable section headers.'
  • Auto Layout makes dynamic content and self-sizing headers much more reliable.
  • Keep the header focused on presentation and route user actions back to the view controller.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.