UIKit essentials · core
UIKit essentials: Auto Layout and lists
Why one label truncates and the other stretches, and why a table of ten thousand rows only ever creates three cells — plus the reuse bug that puts the wrong picture on the wrong row.
By the end you will be able to
- Read a constraint as an equation, and use hugging and compression resistance to decide which view gives way
- Explain cell reuse, and fix the stale-content bug it causes with an asynchronous image load
- Describe diffable data sources and why their identifiers must be stable and unique
A table shows a photo for each row, downloaded as the row appears. Users scrolling quickly report seeing the wrong photo on some rows for a moment. What causes it?
Auto Layout: a constraint is an equation
Every constraint says the same kind of thing:
item1.attribute = multiplier × item2.attribute + constant
titleLabel.leading = 1.0 × view.leading + 16 puts the title 16 points inside the left edge. The = can also be ≥ or ≤, which is what makes constraints a system to be solved rather than a list of assignments. Auto Layout takes them all and finds the frames that satisfy them.
In code you almost always write them with anchors, which are the readable form:
titleLabel.translatesAutoresizingMaskIntoConstraints = false // ← forget this and nothing works
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -16),
])Two failure modes have distinct names, and mixing them up in an interview is noticeable:
- Ambiguous — not enough constraints, so more than one solution fits. The view appears somewhere valid but arbitrary, or with zero size. Diagnose with
view.hasAmbiguousLayout. - Unsatisfiable — too many constraints, and they contradict. Auto Layout prints a long complaint and breaks one of them to carry on, so the layout is wrong in whatever way that broken constraint was preventing.
Priorities, and which view gives way
When constraints compete, priority decides. It is a number from 1 to 1000, and three values matter: 1000 is required, 750 is high and 250 is low.
Two priorities on every view are the ones people get wrong, and they are opposites:
- Content hugging — how hard the view resists growing beyond its natural size. High hugging means "do not stretch me".
- Compression resistance — how hard it resists shrinking below its natural size. High resistance means "do not squash me".
The names describe what the view does to its content: hugging pulls the edges in toward the content, compression resistance pushes back out against being crushed.
Put a title and a badge in a row and you have the classic problem. There is extra space — which one stretches? There is not enough space — which one truncates? Those are two different questions answered by two different priorities:
That is the shape you want in a real row: the badge keeps its exact size in both directions, and the title absorbs whatever space is going and truncates when there is not enough. You get it by giving the badge high hugging and high compression resistance, and leaving the title lower on both.
The interview version of this question is usually phrased as "two labels side by side, why does the wrong one truncate?" The answer is that their compression resistance priorities are equal, so Auto Layout picks arbitrarily — raise the one that must stay whole.
UIStackView
Most rows do not need hand-written constraints at all. UIStackView lays out an array of views along an axis and manages the constraints itself:
Read those three together and U1-02's layout lesson lands differently. SwiftUI's HStack is a stack view with better defaults — and fixedSize() is content hugging under another name. The concepts did not go away; they stopped needing to be spelled out.
Lists: reuse is the whole idea
A table with ten thousand rows does not build ten thousand cells. It builds roughly as many as fit on screen, and recycles them forever:
Eight rows, three cells. Ten thousand rows would still be three. In real UIKit that pool is dequeueReusableCell(withIdentifier:for:), and the identifier is how a table keeps separate pools for separate cell designs.
prepareForReuse() is the wipe. UIKit calls it on a cell just before handing it back out, and your subclass overrides it to clear anything that would otherwise leak from the previous row — an image, a highlighted state, a running animation. What it cannot do is reach into work already in flight.
The bug the pretest describes
Follow the sequence, because this is the one to be able to narrate:
- Row 3 scrolls into view. The table hands you cell #1, and you start downloading row 3's photo.
- The user flicks hard. Row 3 leaves the screen and cell #1 is recycled.
- Cell #1 comes back out for row 47.
prepareForReuseclears the image, and you start downloading row 47's photo. - Row 3's download — started first, still running — finishes. Its completion handler holds cell #1 and sets the image on it.
- Row 47 is now showing row 3's photo. A moment later row 47's own download lands and corrects it, so the wrong picture flashes rather than sticking.
Nothing here is a race between the two downloads. Step 4 would misbehave identically if row 47's photo were already cached.
The repair is a check at the moment of arrival:
Two other ways to say the same thing, both used in production: give the cell a token that changes on every reuse and compare tokens, or cancel the in-flight request inside prepareForReuse. Cancelling is tidier but not sufficient on its own — a request can complete in the instant between the cancel and the recycle — so the arrival check is the one that actually closes the hole.
Diffable data sources
The old way to update a table was to change your array and call reloadData(), which redraws everything: no animation, scroll position disturbed, and the whole list rebuilt because one row's title changed.
A diffable data source takes a snapshot — the list of section identifiers and item identifiers you now want — and works out the difference itself:
var snapshot = NSDiffableDataSourceSnapshot<Section, Item.ID>()
snapshot.appendSections([.main])
snapshot.appendItems(items.map(\.id))
await dataSource.apply(snapshot, animatingDifferences: true)
You describe the destination; UIKit computes the inserts, deletes and moves and animates them:
Two rules come with it, and both are enforced at runtime rather than by the compiler.
Identifiers must be Hashable and stable. The identifier stands for the item across snapshots, so it has to survive the item's contents changing. Use the model's id. Using the array index defeats the entire mechanism, because every insertion renumbers everything after it and the diff reports the whole list as changed.
Identifiers must be unique. Two identical identifiers in one snapshot is a crash, not a warning:
In practice this crash arrives from a backend that sent the same record twice — which makes it the same class of problem as D2-01's version-tolerant decoding: your app can be broken by data it did not create. Deduplicate before you build the snapshot.
Worth knowing that the identifier holds the item's identity, not its contents. Change an item's title without changing its id and the diff sees no difference, so the row does not update. That is what reconfigureItems(_:) is for.
Compositional layout, briefly
UICollectionViewCompositionalLayout describes a collection view's layout as nested groups: items inside groups, groups inside sections, each sized relatively or absolutely. It replaced the flow layout for anything beyond a plain grid, and it is what makes App Store-style screens — a horizontal carousel above a two-column grid above a list — practical without writing a custom layout class.
You are unlikely to be asked to write one from memory. Being able to say what problem it solves is enough at interview.
An engineer builds a diffable snapshot with snapshot.appendItems(Array(0..<items.count)) — the row indices as identifiers. It works. Then rows start animating strangely and the wrong cell expands when tapped. Why?
A row has a title and a right-aligned price. When the title is long, the price truncates instead of the title. Both labels currently use default priorities. What is the fix?
Teach cell reuse to someone who only knows SwiftUI. Start with the problem it solves rather than the mechanism. Explain why the same object serving many rows is a performance win and a correctness hazard at the same time, and use the wrong-photo bug as your worked example. Then make the connection: describe what SwiftUI's List does about the same problem, and why ForEach demands a stable id rather than trusting position.
You can now:
- Read a constraint as an equation, and remember the line that makes code-written layout work at all
- Use hugging and compression resistance to choose which view stretches and which truncates
- Explain cell reuse, and why it makes ten thousand rows cost a handful of cells
- Narrate the wrong-photo bug end to end and write the guard that fixes it
- Describe diffable data sources, and say why identifiers must be stable and unique
Next up: you have both halves of the boundary now. U5-02 is the bridge between them — reread it, and D1-06's hybrid seam shows the same decision at whole-app scale.