Third phase, smaller scope. Two specific changes to the Body tab’s sub-tabs, both directly traceable to APK item layouts:
- KEY STATS cards get an inline sparkline, per
item_side_body_model_card_charts.xml. - MEASURE cards consolidate symmetric pairs (left/right arms, thighs, calves) into a single split card per
item_doble_bottom_body_model_card.xml, and add a derived waist-to-hip ratio card peritem_waist_to_hip.xml.
KEY STATS sparkline
The XML name gave it away: there’s both item_side_body_model_card.xml (plain stat card) and item_side_body_model_card_charts.xml (stat card with inline chart). The APK uses the chart variant when there’s enough scan history to render one; otherwise the plain variant. The chart is small, embedded inside the card, no axes, no labels, just a line showing trajectory.
In SwiftUI with Swift Charts, that’s just:
Chart { ForEach(Array(values.enumerated()), id: \.offset) { i, v in LineMark(x: .value("i", i), y: .value("v", v)) .interpolationMethod(.catmullRom) .foregroundStyle(AppTheme.accent) }}.chartXAxis(.hidden).chartYAxis(.hidden).chartLegend(.hidden).frame(height: 18)The KEY STATS section pulls scan history via @Query(sort: \Scan.createdAt, order: .forward) so each card sees its values in chronological order. Each card filters to its specific metric (history.compactMap { $0.hasBodyFat ? $0.bodyFatRatio : nil }) and renders the sparkline only when there are at least 2 data points. A single dot is noise, not signal; if you only have one scan, the sparkline area on the card is just unused vertical space.
The sparkline color is AppTheme.accent (#ff0040), same hot pink-red as the sub-tab indicator. Consistency: anything that “highlights” or “indicates trend” uses accent; anything that’s a value or label uses one of the dark/white card colors.
MEASURE pair cards
The original iOS implementation rendered 13 horizontally-scrolling cards on MEASURE, one per BodyPart. That’s faithful to the underlying data (every region is its own measurement) but not faithful to the APK’s presentation. The APK has 4 symmetric pairs that share a card each:
- UPPER ARM (
upperArmLeft+upperArmRight) - UPPER THIGH (
upperThighLeft+upperThighRight) - LOWER THIGH (
lowerThighLeft+lowerThighRight) - CALF (
calfLeft+calfRight)
Plus 5 non-symmetric regions (SHOULDER, CHEST, WAIST, HIPS, STOMACH) and the derived WAIST-TO-HIP ratio. 5 + 4 + 1 = 10 cards instead of 13 + 1 = 14: slightly less horizontal scrolling, but more importantly each pair card shows both values side-by-side, which is how the user thinks about that part of their body. “Left upper arm is 35.2 cm” + “Right upper arm is 35.0 cm” reads better as one card with two columns than as two separate cards a swipe apart.
The card design is a small L/R split:
┌────────────────────────┐│ UPPER ARM ││ L R ││ 35.2 cm 35.0 cm │└────────────────────────┘Tapping cycles through three states: nothing selected → left highlighted → right highlighted → nothing. The avatar’s measurement ring updates with each tap, so the user can see the L and R rings independently.
stateDiagram-v2
[*] --> None
None --> Left: tap
Left --> Right: tap
Right --> None: tapThis is more complete than the single-card behavior of “both rings always visible”: the user actually wants to know which side is which when comparing asymmetry.
Waist-to-hip ratio card
Strictly a derived stat (waist_circumference / hip_circumference). Shown only when both base measurements are available; tapping the card focuses the waist ring (since the waist is the more modifiable input of the ratio for most users). Format: String(format: "%.2f", ratio), two decimals, unitless.
The APK has item_waist_to_hip.xml and layout_waist_to_hip_description.xml (a longer descriptive surface, probably reached via an “info” tap). The iOS port has the card but not the description sheet yet; that’s Phase C+ polish if it turns out to matter.
Card ordering
The APK orders the MEASURE cards from top-of-body downward: shoulder, chest, waist, hips, stomach, then arms, then thighs, then calves. iOS now matches that. Waist-to-hip slots in right after hips since it’s anatomically that region.
What this commit does NOT do
- No expanded card view. The APK has
item_bottom_body_model_card_expanded.xmlanditem_bottom_double_card_expanded.xmlfor a tap-to-expand-with-history-graph pattern. Skipping for now: the sparkline on KEY STATS already covers “see the trajectory at a glance,” and the History tab covers “see the full chart.” An expanded variant on MEASURE cards would be a third surface for the same information, low priority. - No animation between symmetric L/R toggling. Tap cycles L → R → off, but the ring change is immediate. The MEASURE camera animation I already had (0.6s ease into region centroid) still runs.
- Sparkline doesn’t show the current scan’s position. Just the line. The APK probably draws a dot for the currently-selected scan; that’s a small enough addition to defer.
Phases A, B, C are in. D is the scan-flow polish: replacing the bare ScanFlowView with the APK’s staged sequence (searching → connecting → scale-align tip → warming up → phone down → countdown → scanning → step off → success/error). That one is real work, about 15 layout XMLs to read through.
Follow along
Procedure 20-ASmall, mechanical phase like the palette post. If you’re adding APK-faithful history and pairing to an existing stat-card UI:
- 1Match the XML variant naming to feature-gate your own views. If the decompiled layout set has both a plain and a “_charts” variant of the same card, that’s your signal for when to show a chart (there’s enough history) versus not (there isn’t).
- 2Guard sparklines on a minimum point count. A single data point isn’t a trend; only render the chart once you have at least two.
- 3Collapse symmetric L/R regions into one card with two columns rather than two separate cards, and cycle a tap through none -> left -> right -> none to select which side’s ring shows on the avatar.
- 4Derive ratio stats (like waist-to-hip) from existing values rather than storing them, and route their “tap to focus” behavior to whichever base measurement is more actionable for the reader.
- 5Match the APK’s card ordering (read it from the layout XML, top-of-body downward) instead of guessing from a screenshot.
- 6Write down the deliberate gaps (no expanded view, no current-scan marker on the sparkline, etc.) so the next phase starts from an accurate list, not a rediscovery.
Agent prompts Procedure 20-A as prompts for a coding agent · 6 prompts
Paste into Claude Code or a similar agent, one at a time, from the root of your iOS project. Each prompt maps to the step with the same number above.