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
flowchart LR
Storyboard["Main.storyboard"] --> Controller["ViewController"]
Controller -->|"strong private owner"| DataSource["CustomDataSource"]
Controller -->|"weak outlet"| Table["UITableView"]
Table -->|"weak dataSource callback"| DataSource
DataSource -->|"register / dequeue / configure"| Cell["CustomCell"]
Cell --> Headline["headlineLabel"]
Cell --> Body["bodyLabel"]
Dynamic["Content-size category"] --> Cell
Cell -->|"intrinsic sizes + constraints"| Height["Self-sized row height"]
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
classDiagram
ViewController o-- UITableView : weak IBOutlet
ViewController *-- CustomDataSource : private let
UITableView --> CustomDataSource : weak callback reference
CustomDataSource *-- SampleTupleArray : immutable sampleData
UITableView *-- CustomCell : reuse lifecycle
CustomCell *-- UILabel : headlineLabel
CustomCell *-- UILabel : bodyLabel
CustomCell *-- NSLayoutConstraint : active layout graph
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 |