Use generated types directly in the iOS app instead of a hand-written mirror model
Decision
Use generated types directly in services, views and helpers. A type that Apollo codegen produces from a GraphQL fragment, or that UniFFI exports from the shared Rust core, is the model layer, and we write no parallel Swift type that mirrors it. Display logic and computed properties go in extensions on the generated type.
A view reads any piece of data from one source: an @Observable service in the environment for live data, or a parameter for a historical snapshot the service doesn't hold. A view that takes a value as a parameter and also reads it from a service has two sources for one thing.
For data that still goes through Apollo's normalized cache, an object that exposes an id is keyed by its typename and id, so every operation that returns the same object updates the same cache record. Value types with no id keep Apollo's default path-based keys, because they have no identity to share between queries. A type whose id is not unique across the queries that return it opts out of normalization explicitly.
Why
Early on the app kept hand-written Swift models mirroring Apollo's generated types, with initializers converting between them. They existed because the generated types could not safely cross concurrency boundaries. Apollo fixed that, and the mirror layer had no reason left. A generator already keeps its output in step with the schema. A hand-kept mirror has to be edited for every schema change the generator absorbs for free, and its conversion code spreads into every service and view.
Rejected alternatives
- A hand-written model mirroring the generated types. Every schema change is edited twice, and conversion code lands in every service and view.
- Apollo's default path-based cache keys. A mutation's result lands under its own operation path, apart from the query a view is watching, so the view stays stale until someone refetches by hand.
Consequences
A schema change means editing a fragment, an extension or the shared core, with no mirror to update. A Swift type that models real behaviour, such as the cases of a union, is a domain type and stays. An id added to a type changes its cache identity everywhere, so that field matters to every query and mutation that returns the type.