Squirreleye LLC Squirreleye Service Notes Naked Labs Mirror · Revival Series Index Tools About
21
Section 21 · follows 20

Scan flow chrome: one icon-title-body-action shape, applied seven times

Written
2026-05-17

The APK has roughly fifteen scan-flow fragments: fragment_searching, fragment_connecting, fragment_scale_align, fragment_warming_up, fragment_phone_down, fragment_get_ready, fragment_countdown, fragment_scanning, fragment_step_off, fragment_scan_success, fragment_scan_timed_out, fragment_error_generic, fragment_error_tip, fragment_warning_timing_out, and fragment_pick_scanner. Reading them in sequence, they all share the same five-element template:

┌─────────────┐ │ │ │ [icon] │ ← centered, ~64sp, accent color │ │ │ Title │ ← bold, ~title2, white │ │ │ Body copy │ ← body, secondaryText │ │ │ [action] │ ← if any: button or progress indicator │ │ └─────────────┘

Different icon, different copy, sometimes a button or progress indicator at the bottom, sometimes nothing. Background is the same flat tab_bg = #052235 everywhere. Type scale is the same. The pattern is so consistent that the APK probably has it as a shared XML include, though the layouts each include the full surface for fragment-isolation reasons.

The iOS port had been improvising. ConnectingView was a tiny VStack with an indeterminate progress wheel. WaitForScaleView was a different VStack with a 60sp scalemass icon and a hand-rolled timeout indicator. AligningView was yet another VStack. Each had .padding(32) or .padding() ad-hoc, .purple accents from an earlier theme draft, .white ProgressView tints, and a .black background that didn’t match the rest of the app.

Phase D’s job: pull the shared shape out into a ScanFlowChrome view and apply it.

My ScanFlowView state machine has nine inner states, and the chrome needed to cover all of them:

Fig. 21-1
stateDiagram-v2
    [*] --> connecting
    connecting --> selectMirror
    selectMirror --> waitForScale
    waitForScale --> preflight
    preflight --> alignScale
    alignScale --> confirmAlignment
    confirmAlignment --> scanning
    scanning --> processing
    processing --> complete
    processing --> failed
    complete --> [*]
    failed --> [*]
§ 21.1

The chrome

Fig. 21-2
swift
struct ScanFlowChrome<Content: View>: View { let icon: String let title: String let subtitle: String? @ViewBuilder let content: () -> Content var body: some View { VStack(spacing: 24) { Spacer() Image(systemName: icon) .font(.system(size: 64, weight: .regular)) .foregroundStyle(AppTheme.accent) Text(title) .font(.title2.bold()) .foregroundStyle(.white) .multilineTextAlignment(.center) if let subtitle { Text(subtitle) .font(.subheadline) .foregroundStyle(AppTheme.secondaryText) .multilineTextAlignment(.center) .padding(.horizontal, 32) } content() Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 24) }}

The @ViewBuilder content slot at the bottom is where each fragment’s specific action goes: a ProgressView, a button, a status row, a list of detected mirrors, whatever. The rest is the same across all of them. Using this for ConnectingView:

Fig. 21-3
swift
ScanFlowChrome(icon: "dot.radiowaves.left.and.right", title: "Looking for your mirror", subtitle: "Bluetooth scan in progress. Make sure your mirror is powered on and within 10 feet.") { ProgressView().tint(AppTheme.accent).scaleEffect(1.2)}

Five lines, instead of the previous version’s ad-hoc VStack. Every other state can adopt the same pattern.

§ 21.2

The palette sweep

While the inner views still have their own bodies (they need state-specific layouts: MirrorPickerView’s List, ScanningView’s progress UI), I did a global find-replace pass on the obvious tokens:

  • .foregroundStyle(.purple) → .foregroundStyle(AppTheme.accent) (4 occurrences). The icons were all using the earlier .purple from when the accent was a placeholder. Now they’re consistently #ff0040 matching the rest of the app.
  • ProgressView().tint(.white) → ProgressView().tint(AppTheme.accent) (5 occurrences). The progress spinners now feel like part of the same UI rather than generic system widgets.
  • The ScanFlowView root background: Color.black.ignoresSafeArea() → AppTheme.background.ignoresSafeArea(). So now scan flow uses the same #052235 as the rest of the app.

That’s it for the global sweep. The per-state layouts (mirror list, alignment-confirm two-button row, scan progress, etc.) keep their unique shapes but inherit the new palette.

§ 21.3

What’s deferred

The APK has fifteen scan-flow fragments. iOS implements about half. Specifically missing as their own states:

  • fragment_warming_up: between BLE connection and the first scan action. The APK shows a “warming up the sensors” screen for a few seconds; iOS just transitions immediately.
  • fragment_phone_down: “put your phone down on the floor / surface so we don’t see you holding it during scan.” I assume the user reads the laser-on instructions instead.
  • fragment_countdown: 3, 2, 1 timer before scan starts. iOS goes straight to scanning once init+scanToken are written.
  • fragment_step_off: between scan complete and processing, “step off the scale now.” iOS just shows ProcessingView.
  • dialog_scale_align_tip: pop-up with alignment instructions. iOS expects users to figure out from the on-screen text.

Each of these would need real BLE state-machine work to wire up correctly, not just UI. The mirror sends specific characteristic value transitions for some of them (scanState changes from .warmingUp to .scanning to .processing), and adding screens without correctly observing those signals would mean stuck states. Phase D scope was UI polish for the existing flow; adding new BLE-gated phases is its own atomic commit.

When that commit lands, the chrome is already in place. Each new state just needs a ScanFlowChrome(icon:, title:, subtitle:) invocation with the right copy. That’s the value of pulling the shape out now.

Phase E next: the first-scan walkthrough. Roughly 10 OOBE fragments (fragment_first_scan_intro, fragment_first_scan_prepare, fragment_first_scan_pose, etc.) that introduce a new owner to the scanning process. iOS currently has a single FirstLaunchSheet that asks for name/height/units; the APK walks through the whole “here’s what to expect when you scan” sequence. That’s the next phase.

§ 21.4
§ 21.5
Agent prompts Procedure 21-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.

1
Find the repeated fragment shape
Decompile the APK and grep `res/layout` for a family of screen fragments that repeat the same icon/title/body/optional-action shape (here, the ~15 scan-flow fragments: `fragment_searching`, `fragment_connecting`, `fragment_scale_align`, etc). Before writing or modifying individual SwiftUI screens for each state, confirm whether they all share one template rather than treating each as a one-off layout.
2
Extract shared chrome view
Create a parameterized SwiftUI view (e.g. `ScanFlowChrome<Content: View>`) taking `icon: String`, `title: String`, `subtitle: String?`, and an `@ViewBuilder content: () -> Content` slot for the one part that differs per screen (a `ProgressView`, a button, a status row, etc). Rebuild each ad hoc state view in the scan-flow state machine (e.g. `ConnectingView`) to call this chrome instead of duplicating its own `VStack`/padding/font setup.
3
Sweep stale placeholder colors
Grep the scan-flow view files for leftover placeholder styling that predates the real theme — e.g. `.foregroundStyle(.purple)`, `ProgressView().tint(.white)`, `Color.black.ignoresSafeArea()` — and replace each occurrence with the corresponding `AppTheme` token (`AppTheme.accent`, `AppTheme.background`, etc). Report the number of replacements made per pattern.
4
Separate UI polish from BLE state wiring
For any scan-flow screen that depends on a specific protocol/BLE signal to be correct (e.g. a `scanState` characteristic transition like `.warmingUp` → `.scanning`), do not add that screen to the UI in this commit unless the signal observation is also wired up. Leave a note identifying which deferred states (e.g. warming-up, phone-down, countdown, step-off) require real state-machine work, and treat adding them as a separate commit from pure visual polish.