The final phase. Two surfaces: MoreTab (the bottom-nav More entry: links to scanner status, account, history, calibration, scan tips, about) and HistoryView (a chart + a list of past scans + a compare button).
Both were using the iOS system list styling (default grouped style on a system-grouped background), which on iOS 18 renders as light-gray section dividers on a slightly-lighter-gray background, with white card insets behind each section. That’s fine on system-themed apps. It looks out of place on top of #052235 navy.
The APK version is much simpler visually: flat navy background everywhere, section headers as small-caps muted-white text, rows white-on-navy with subtle dividers between them. The structure is the same as iOS’s grouped list. The styling is what differs.
SwiftUI’s “make this list transparent” magic chord
List { Section { ... } header: { Text("SCANNER").foregroundStyle(AppTheme.secondaryText) }}.scrollContentBackground(.hidden).background(AppTheme.background).scrollContentBackground(.hidden) is the modifier that hides the default list/form background (iOS 16+). Without it, you can’t see your custom .background through the list. The system one wins. With it, your custom background shows through, and you get the dark-navy look you wanted.
Bonus catch: .scrollContentBackground is also the right tool for Form. If you start putting .background on a Form and wondering why it does nothing, this is why.
The navigation bar’s three-modifier dance
The iOS navigation bar needs three separate modifiers to look right on a dark theme:
.toolbarBackground(AppTheme.background, for: .navigationBar).toolbarBackground(.visible, for: .navigationBar).toolbarColorScheme(.dark, for: .navigationBar).toolbarBackground(color, for:): set the background color..toolbarBackground(.visible, for:): force the background to render even when content isn’t scrolled under it. Without this, the bar stays translucent and shows the underlying content (which is your navy bg, so it half-works) but doesn’t apply the explicit color above..toolbarColorScheme(.dark, for:): force the bar’s text and chevrons to render as if it were a dark-mode surface, regardless of the rest of the app’s color scheme.
The first two are similar to the bottom-tab dance I already did in Phase A:
.toolbarBackground(AppTheme.bottomBarBg, for: .tabBar).toolbarBackground(.visible, for: .tabBar)Same pattern, different bar. The third (toolbarColorScheme) is only needed for navigation bars where back-button chevrons would otherwise default to system tint. Tab bars don’t have that problem.
HistoryView chart restyling
Swift Charts inherits a lot from the system theme. LineMark defaults to the app’s tint color; PointMark defaults to the same. On the dark navy bg they render as a thin blue-purple line that’s hard to see.
LineMark(x: .value("Date", scan.createdAt), y: .value(metric.rawValue, v)) .interpolationMethod(.catmullRom) .foregroundStyle(AppTheme.accent)PointMark(x: .value("Date", scan.createdAt), y: .value(metric.rawValue, v)) .foregroundStyle(AppTheme.accent).foregroundStyle(AppTheme.accent) on both marks gives the hot-pink-red line, matching every other selection/highlight surface in the app.
The axis tick labels need their own pass:
.chartXAxis { AxisMarks { _ in AxisValueLabel().foregroundStyle(AppTheme.secondaryText) AxisGridLine().foregroundStyle(.white.opacity(0.1))}}.chartYAxis { AxisMarks { _ in AxisValueLabel().foregroundStyle(AppTheme.secondaryText) AxisGridLine().foregroundStyle(.white.opacity(0.1))}}The default AxisMarks use the system foreground style (black-ish on light, white-ish on dark), but Swift Charts in iOS 18 renders against a white-ish chart-plot background by default, so the default labels would be invisible on my navy bg. Setting both AxisValueLabel (the numeric tick labels) and AxisGridLine (the faint horizontal/vertical reference lines) explicitly is what gives you a chart that looks like it belongs.
10% white opacity for the grid lines is barely visible: enough to give the chart depth without competing with the data. The APK’s chart has even subtler grid lines; 10% is close enough.
The list rows inside HistoryView
List { ForEach(scans.reversed(), id: \.id) { scan in HStack { Text(scan.formattedDate).foregroundStyle(.white) Spacer() Text(Units.weight(scan.weight)) .foregroundStyle(AppTheme.secondaryText).monospacedDigit() Text(String(format: "%.1f%%", scan.bodyFatRatio)) .foregroundStyle(AppTheme.secondaryText).monospacedDigit() } .listRowBackground(Color.white.opacity(0.04)) }}.listStyle(.plain).scrollContentBackground(.hidden).listStyle(.plain) removes the inset gray section cards; .listRowBackground(Color.white.opacity(0.04)) puts a very faint white wash behind each row so they don’t disappear into the navy. This is one of the patterns the APK uses: a hairline-tinted row surface over a dark background, so individual rows feel discrete without needing strong dividers.
What was NOT polished
Most of the other inner surfaces (ProfileEditView, CalibrationView, AboutView, MirrorSettingsView) still use the iOS system Form/List styling on system backgrounds. They’re functional, they’re just not APK-faithful in palette.
Doing those individually didn’t seem worth a separate phase. The pattern is the same as MoreTab: three modifiers (toolbarBackground, scrollContentBackground, toolbarColorScheme) plus secondaryText colors on labels. Anyone reading this can apply it. Phase F’s contribution is establishing the pattern and applying it to the two highest-visibility surfaces.
Six phases in
Phase A (palette + chrome), Phase B (Compare rebuild), Phase C (KEY STATS sparklines + MEASURE L/R cards + waist-to-hip), Phase D (scan-flow chrome + palette sweep), Phase E (first-scan walkthrough), Phase F (MoreTab + HistoryView dark-list pattern). Each one a separate commit, each one with its own blog draft.
The app now looks like a deliberate port of the Android app rather than a custom-styled approximation. Same palette, same accent, same structural shape on Compare and Body tabs, same prep content on the walkthrough, same chart accent on History. The visible gaps from here are scanner-pairing UX (Phase D’s deferred BLE-gated screens), per-region history-graph expansions on MEASURE cards (Phase C’s deferred expanded variant), and styling polish on ProfileEditView / CalibrationView / AboutView / MirrorSettingsView. None of those block daily use; all are visible polish for whenever next.
Follow along
Procedure 23-AThe dark-navy conversion for a SwiftUI list/chart surface comes down to four small, reusable pieces:
- 1Hide the system list/form background and supply your own.
.scrollContentBackground(.hidden)plus.background(AppTheme.background)on anyListorFormthat needs to sit on a non-system color. - 2Apply the three-modifier bar dance for navigation bars, two for tab bars. Navigation bars need
.toolbarBackground(color, for:),.toolbarBackground(.visible, for:), and.toolbarColorScheme(.dark, for:); tab bars only need the first two. - 3Override Swift Charts’ default styling explicitly. Set
.foregroundStyleon yourLineMark/PointMark, and setAxisValueLabel/AxisGridLinecolors inside.chartXAxis/.chartYAxis. The defaults assume a light chart-plot background and will be invisible on a dark one. - 4Use
.listStyle(.plain)plus a faint.listRowBackground(something likeColor.white.opacity(0.04)) instead of the default inset grouped cards, if you want individual rows to read as distinct without heavy dividers.
Agent prompts Procedure 23-A as prompts for a coding agent · 4 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.