The iOS app had been shipping with a custom-styled palette I’d reverse-engineered from CNET screenshots and the demo video: navy gradient #11213d → #1a304f, pink accent #e83d57. That was a good-enough approximation while the project was about getting scans to actually work. Now that scans work and the UI overflow bug is fixed, the question becomes: do the iPhone screens feel like the Android app’s screens? And the answer was no, mostly because my color values were off by 10-20% on every axis.
apktool d com.nakedlabs.android.nakedfit.apk and a grep -E '"(tab_bg|tab_indicator|secondary|bottom_bar)"' res/values/colors.xml gave me the real answers in about 90 seconds:
<color name="tab_bg">#052235</color><color name="secondary">#ff0040</color><color name="bottom_bar">#14131b</color><color name="unselected_tab_text">#b3f5f6f7</color><color name="gray_compare_difference">#8014131b</color><color name="gray_compare_item_background">#ececf0</color><color name="red">#e47373</color>A few non-obvious findings dropped out of those seven lines.
flowchart LR
A[nakedfit.apk] -->|apktool d| B[res/values/colors.xml]
B -->|grep tab_bg, secondary, bottom_bar, ...| C[7 real hex values]
C -->|Color hex: initializer| D[AppTheme enum in SwiftUI]The background isn’t a gradient. I had LinearGradient(navyTop, navyBottom) from top to bottom of every screen. The APK is a flat #052235. Looking at the demo video keyframes again with this in mind, the apparent vertical tint was actually the demo room’s overhead lighting reflecting off the phone screen, not a gradient in the app itself. The gradient was a memory artifact of looking at lit-up phone screenshots.
The accent is much more saturated than I’d matched. APK #ff0040 is essentially pure red with a touch of magenta: sharp and high-energy. My #e83d57 was a softer coral-pink. They look like the same color until you put them next to each other, and then it’s obvious the APK version pops harder.
There’s a separate red token that isn’t the accent. #e47373 is a warmer, dustier red reserved for warning/error states, explicitly not used for tab indicators or selection. Conflating those was something my custom palette had been doing.
Compare-item background is #ececf0, not white. A very light cool-gray that sits on top of the dark navy main background. The before/after stat values are on this light gray surface, not on the navy. The compare delta value is #8014131b, which is the bottom_bar color at 50% opacity. That kind of color reuse (one base, used at varying opacities for different roles) is exactly the kind of detail you only get by reading the source rather than eyeballing.
Tab indicator height is 3.0dp, not 2. A 50% thicker underline than I had. Small change, but the original feels meaningfully more present once you fix it.
The translation to SwiftUI:
extension Color { init(hex: UInt32, alpha: Double = 1.0) { let r = Double((hex >> 16) & 0xff) / 255.0 let g = Double((hex >> 8) & 0xff) / 255.0 let b = Double( hex & 0xff) / 255.0 self.init(.sRGB, red: r, green: g, blue: b, opacity: alpha) }} enum AppTheme { static let background = Color(hex: 0x052235) static let accent = Color(hex: 0xff0040) static let bottomBarBg = Color(hex: 0x14131b) static let compareRowBg = Color(hex: 0xececf0) static let compareDelta = Color(hex: 0x14131b).opacity(0.5) static let warningRed = Color(hex: 0xe47373) static let secondaryText = Color.white.opacity(0.70) static let cardLabel = Color(hex: 0x14131b).opacity(0.7)}That Color(hex:) initializer is a habit worth keeping. Apple has never shipped a hex initializer on Color despite a decade of every iOS codebase needing one, so most projects roll their own. Using the same shape 0xRRGGBB that the APK XML uses means 0xff0040 reads as the same string in the iOS source and in the Android source: when I’m cross-referencing the two, I never have to mentally parse Color(red: 1.0, green: 0.0, blue: 0.25) against #ff0040. The shapes match. That matters more than you’d expect during a port.
Why the backgroundGradient shim stays for now. The previous code had AppTheme.backgroundGradient used in three different views as .background(AppTheme.backgroundGradient.ignoresSafeArea()). Phase A’s mandate was “fix the palette without restructuring the views.” So instead of doing a sweep, I kept the symbol but redefined it as a single-color “gradient” using the new flat background. The visual result is identical to a flat background, and the call sites don’t need to change. Phase B and later will migrate them to AppTheme.background directly as they touch each view for other reasons.
The iOS system bottom TabBar got .toolbarBackground(AppTheme.bottomBarBg, for: .tabBar) + .toolbarBackground(.visible, for: .tabBar). The second modifier is critical: without it iOS keeps the translucent effect even when you specify a color, so the tab bar reads the main background through it instead of showing the dark #14131b.
This was the smallest possible commit in the UI-faithfulness sequence: no screen restructuring, no new views, no behavior changes. Just the palette. The next post covers the Compare tab rebuild, which is a real port: the APK Compare screen has a date-picker structure and per-stat tap-to-zoom that don’t exist in the iOS app at all.
Follow along
Procedure 18-AThis post is a small, mechanical change rather than a debugging journey, so there’s not much journey to preserve. If you want to pull the real palette out of an Android APK for your own faithful port:
- 1Decode the APK with apktool, not just strings/grep:
apktool d <package>.apk. - 2Grep the resource XML for the tokens you care about:
grep -E '"(tab_bg|tab_indicator|secondary|bottom_bar)"' res/values/colors.xml. Android color resources are plain#AARRGGBBor#RRGGBBhex strings. - 3Watch for alpha-prefixed hex (
#80xxxxxx): that’s an existing color token reused at a given opacity, not a new color. Resolve it back to its base token plus an opacity modifier rather than treating it as a one-off value. - 4Write a
Color(hex:)initializer that takes the same0xRRGGBBshape as the Android resource file, so you can eyeball-diff your SwiftUI theme against the XML without mentally converting formats. - 5Check
res/values/dimens.xmlfor any size the APK treats as a constant (stroke widths, fixed heights) rather than guessing from a screenshot. - 6Don’t restructure views in the same commit. If an old symbol (like a gradient helper) is used in several places, keep its name and swap its definition first; migrate call sites later when you’re touching those views for other reasons anyway.
Agent prompts Procedure 18-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.