Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Creating self-sizing table view cells

At a glance

Item Summary
Purpose Builds table cells whose fonts, spacing, and height adapt to Dynamic Type and multiline content.
App architecture A view controller retains an extracted data source; the data source registers/configures cells; each cell owns a complete intrinsic-content constraint chain.
Main patterns Extracted data source, intrinsic-content sizing, Dynamic Type, label factory, and constraint-driven layout.
Project style Four-file UIKit sample with the architecture intentionally centered on CustomCell rather than application lifecycle.

Project structure

SelfSizingCellsWithDynamicType/
├── AppDelegate.swift
├── ViewController.swift
├── CustomDataSource.swift
├── CustomCell.swift
├── Base.lproj/Main.storyboard
└── Localizable.stringsdict

Structure observations

  • Controller wiring, row data, and cell layout are separate roles.
  • CustomCell.swift is the feature core; AppDelegate explicitly says it is not central to the sample.
  • Localization data is separate from the data source that formats the pluralized headline.

Overall architecture

Reference code

SelfSizingCellsWithDynamicType/ViewController.swift:16 — the controller keeps the data source alive, lets it register its own cell type, and then assigns it to the table.

@IBOutlet weak var tableView: UITableView!

private let dataSource = CustomDataSource()

override func viewDidLoad() {
    super.viewDidLoad()

    dataSource.registerCells(with: tableView)
    tableView.dataSource = dataSource
}

Interpretation

ViewController is only a composition boundary. Row knowledge stays in CustomDataSource, and all sizing knowledge stays in CustomCell; UIKit calculates height from label intrinsic content sizes and the cell’s complete vertical constraints.

Ownership and state

Ownership evidence

SelfSizingCellsWithDynamicType/CustomCell.swift:29 — the cell creates and retains both labels, and both initializer paths run one private setup routine.

let headlineLabel = makeSampleLabel()
let bodyLabel = makeSampleLabel()

override init(
    style: UITableViewCell.CellStyle,
    reuseIdentifier: String?
) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    setUpLabelsAndConstraints()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setUpLabelsAndConstraints()
}
Owner Object or state Relationship Mutation authority
ViewController CustomDataSource Strong, stable private let Controller initialization only
Storyboard/controller tableView View hierarchy owns it; controller holds weak outlet Storyboard and view lifecycle
CustomDataSource Immutable sample tuples Strong value ownership Declaration only
UITableView Reusable cell instances Framework reuse ownership Dequeue/reuse lifecycle
CustomCell Headline/body labels and active constraints Creates; content view retains subviews Cell initialization

The controller’s strong data-source property matters because UIKit’s dataSource reference is non-owning; a temporary data source would disappear after setup.

Class and protocol design

Type or protocol Responsibility Depends on or conforms to
ViewController Connect table and long-lived data source UIViewController
CustomDataSource Own sample rows, register CustomCell, format and bind text UITableViewDataSource
CustomCell Create labels, configure Dynamic Type, and establish complete self-sizing constraints UITableViewCell
AppDelegate Hold legacy application window UIApplicationDelegate

There is no app-defined protocol. The only callback abstraction is UITableViewDataSource, and the concrete data source is deliberately small.

Access control

Symbol Access Verified effect Why it fits this design
ViewController.dataSource private let Only the controller can access or replace its strong callback owner. Protects lifetime and keeps table wiring local.
CustomCell.makeSampleLabel private static Shared only by the cell’s stored-property initialization. Hides base label construction.
CustomCell.setUpLabelsAndConstraints private Only initializer paths can invoke layout setup. Prevents duplicate subviews/constraints after initialization.
headlineLabel and bodyLabel implicit internal let The separate data-source file can set text, but cannot replace labels. Enables direct teaching-sample binding; a production cell might expose configure instead.
reuseIdentifier and sampleData implicit internal Available to app-module collaborators. Registration/configuration spans cell and data-source files.
tableView implicit internal weak outlet Non-owning controller reference to storyboard-owned view. Avoids extending view lifetime outside hierarchy.
App types implicit internal No external framework API. The sample is a standalone app.

Reference code

SelfSizingCellsWithDynamicType/CustomCell.swift:37 — construction mechanics stay private while the resulting labels are exposed read-only for row binding.

private static func makeSampleLabel() -> UILabel {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.numberOfLines = 0
    return label
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Data-source lifetime and table connection ViewController Requires the outlet and view lifecycle.
Row count, registration, localization, text binding CustomDataSource All row/cell creation knowledge lives behind one data-source contract.
Label construction and Dynamic Type fonts CustomCell Reusable appearance belongs to the reusable view.
Vertical system spacing and self-sizing chain CustomCell Cell alone owns its content hierarchy and constraints.
Height calculation Auto Layout / UITableView Framework derives it from intrinsic content and constraints.

Design patterns

Pattern Source evidence Purpose or tradeoff
Extracted data source SelfSizingCellsWithDynamicType/CustomDataSource.swift:12 Keeps row creation/data outside the view controller.
Strong callback owner SelfSizingCellsWithDynamicType/ViewController.swift:18 Balances the table’s non-owning data-source reference.
Factory method SelfSizingCellsWithDynamicType/CustomCell.swift:37 Applies identical base configuration to two labels.
Dynamic Type SelfSizingCellsWithDynamicType/CustomCell.swift:75 Uses preferred/scaled fonts and automatic category adjustment.
Constraint-driven self-sizing SelfSizingCellsWithDynamicType/CustomCell.swift:123 Completes top-to-bottom baseline constraints using system spacing.
Cell registration by data source SelfSizingCellsWithDynamicType/CustomDataSource.swift:32 Lets the owner of cell creation own registration knowledge.

Naming conventions

  • CustomCell and CustomDataSource use role suffixes, though “Custom” is generic because the sample is narrowly scoped.
  • headlineLabel and bodyLabel name semantic typography roles.
  • makeSampleLabel is a factory verb; setUpLabelsAndConstraints describes one-time construction.
  • reuseIdentifier and registerCells(with:) mirror table-view vocabulary.

Architecture takeaways

  • Keep a strong owner for weak UIKit callback properties.
  • Let the data source register/configure the cell types it creates.
  • Put a complete constraint chain and Dynamic Type configuration inside the reusable cell.
  • Expose label references only when direct binding is intentional; otherwise prefer a semantic configure method.
  • Self-sizing is an Auto Layout result, not controller-maintained row-height state.

Source map

Source file Relevant symbols
SelfSizingCellsWithDynamicType/ViewController.swift Composition and data-source lifetime
SelfSizingCellsWithDynamicType/CustomDataSource.swift Sample rows, registration, binding
SelfSizingCellsWithDynamicType/CustomCell.swift Dynamic Type and self-sizing constraints
SelfSizingCellsWithDynamicType/AppDelegate.swift Application/window lifecycle