Squirreleye LLC Squirreleye Service Notes Naked Labs Mirror · Revival Series Index Tools About
18
Section 18 · follows 17

Reading the APK's colors out of res/values/colors.xml

Written
2026-05-17

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:

Fig. 18-1
xml
<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.

Fig. 18-2
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:

Fig. 18-3
swift
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.

§ 18.1
§ 18.2
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.

1
Decode APK with apktool
Run `apktool d <package>.apk` on the target Android APK to get its decompiled resources rather than relying on `strings`/`grep` against the raw binary. Confirm the output includes `res/values/colors.xml` and `res/values/dimens.xml` before moving on.
2
Grep colors.xml for hex values
In the decoded APK's `res/values/colors.xml`, run `grep -E '"(tab_bg|tab_indicator|secondary|bottom_bar)"' res/values/colors.xml` (substitute the token names relevant to your screens) to pull the real `#AARRGGBB`/`#RRGGBB` hex values Android uses, instead of eyeballing colors from screenshots. Record each token name alongside its hex value.
3
Resolve alpha-prefixed hex tokens
For any color value in `colors.xml` with an 8-digit hex (`#AARRGGBB`, e.g. `#8014131b`), do not treat it as a new standalone color. Identify the matching 6-digit base token elsewhere in the file (here, `#8014131b` is `bottom_bar` = `#14131b` at the leading `0x80` alpha, i.e. 50% opacity) and model it in Swift as that base color with an `.opacity()` modifier rather than a separate `Color(hex:)` literal.
4
Add a Color(hex:) initializer
Add a `Color(hex: UInt32, alpha: Double = 1.0)` initializer to the SwiftUI codebase (extension on `Color`) that decodes a `0xRRGGBB` literal into sRGB red/green/blue components, matching the same hex shape Android's `colors.xml` uses. Then define (or update) an `AppTheme` enum with `static let` properties for each resolved token — e.g. `background`, `accent`, `bottomBarBg`, `compareRowBg`, `compareDelta`, `warningRed` — each initialized from the hex values pulled from the APK, so the Swift source and the Android XML use the literal same hex digits.
5
Check dimens.xml for literal sizes
Grep the decoded APK's `res/values/dimens.xml` for any size constant relevant to the screens you're porting (stroke widths, fixed heights, indicator thickness) rather than estimating them from a screenshot. Apply each literal value directly in the SwiftUI layout (e.g. a `3.0dp` tab-indicator height becomes a `3`-point stroke width).
6
Keep old symbol names, swap definitions
If an existing SwiftUI symbol (e.g. a `backgroundGradient` helper) is referenced across multiple views and you're fixing its underlying value without restructuring those views yet, keep the symbol's name and change only its definition (e.g. redefine a `LinearGradient` as a flat single-color fill). Do not touch call sites in this commit; leave migrating them to a direct `AppTheme.background` reference for a later commit that's already touching those views for other reasons.