Diffable Data Source Identity in a Flash Sale
The Home screen drives a UICollectionViewDiffableDataSource<Section, HomeItem> where HomeItem is an enum with associated values: banner(Banner), product(Product), ad(Ad). Product has id: UUID, title, price, rating, and imageURL. Prices refresh from a socket every 30 seconds. During a flash sale the app crashes with 'supplied item identifiers are not unique', reproducibly when two different products share a title; the team's custom Hashable/Equatable conformance uses only title. Separately, another team wants to ship new Home item kinds from its own module without editing the Home module. Today adding one case means touching six switch statements, three of them in another target.
1. Diagnose the duplicate-identifier crash and state the identity rule a diffable data source needs. What happens if you also hash `price`?
Read the worked answer
A diffable data source treats the identifier's Hashable/Equatable conformance as object identity: two items are 'the same row' exactly when they compare equal. A conformance based on title declares that two products with the same title are one row, so the snapshot contains two equal identifiers and UIKit asserts. The crash is a correct complaint about a wrong model, not a UIKit quirk.
The rule: identity must derive from a stable unique key - here id - and nothing else, excluding mutable display fields. hash(into:) should feed id; == should compare id.
Including price is the opposite failure. No crash, but each 30-second refresh makes a changed product compare unequal to its previous self, so the data source applies delete+insert rather than reconfigure. You get flicker, lost selection, broken animation, and image reloads. That is the same root cause as the crash - conflating content with identity - and it is worth calling out because the 'obvious' fix of hashing everything reintroduces it.
2. Evaluate enum-with-associated-values versus a protocol-based model for the extensibility requirement. Include the concrete Swift obstacles.
Read the worked answer
The enum buys exhaustive switching: add a case and the compiler points at every site that must handle it. Excellent for a closed set, terrible for a set that another module must extend, because the compiler's help becomes a cross-module edit - exactly the coupling reported. Swift also cannot add a case from an extension in another module, so the second team cannot extend the enum at all.
A protocol model -
protocol HomeItem: Hashable, Identifiable { var id: UUID { get } }
- lets each module define its own types. Obstacles: (a)
any HomeItemis not directly usable asItemIdentifierTypewhen the protocol has associated types orSelfrequirements;Identifiableis fine here, but aHashableexistential generally needsAnyHashableor a small erasing wrapper that forwardshash(into:)and==onidonly. (b) You lose exhaustiveness, so rendering becomes dynamic dispatch - a visitor, or a registry keyed by kind. (c) New kinds can arrive from the server at runtime that the Home module has no cell for, so a fallback is mandatory. (d) Equality must be enforced consistently, or two distinct concrete types sharing a UUID collide.
3. Recommend a design satisfying both teams, and say what you would do about the six switch statements.
Read the worked answer
Separate identity from content:
struct HomeItemID: Hashable { let raw: UUID; let kind: String }
struct HomeItem: Hashable, Identifiable {
let id: HomeItemID
let content: HomeContent
}
Hash only id. Register cell providers in a dictionary keyed by kind - [String: (UICollectionView, IndexPath, HomeItem) -> UICollectionViewCell?] - populated at launch by whichever module owns that kind. A new team registers its own kind and ships its own cell; the Home module stops switching entirely, and an unknown kind renders a harmless placeholder instead of crashing, which matters because a client build can lag the server.
If you keep the enum, @unknown default gives a fallback for non-frozen enums from another module, but the enum still needs the case to exist - which the other module cannot add.
The trade-off, stated honestly: the registry gives up compile-time exhaustiveness. You buy it back with a startup test asserting every kind the server can emit has a registered provider. Given a genuine cross-team extension point, that is the better trade; for a closed, app-owned set, keep the enum.