Skip to content

Latest commit

 

History

History
391 lines (291 loc) · 24.8 KB

File metadata and controls

391 lines (291 loc) · 24.8 KB

Weekly UX Review — 2026-05-11

App: Pip's Kitchen Garden (ChefAcademy)
Reviewer: Claude Code (automated weekly audit)
Scope: Full codebase — all 88 Swift source files read
Audience lens: Ages 6+ (per March 2026 UX audit revision)
Anti-pattern categories: A=Hidden-mode taps · B=Multi-step purchases · C=Missing affordances · D=Tap target size · E=Off-screen actions · F=Text-heavy/readability · G=Inconsistent gestures · H=Missing next-step cue · I=No undo · J=Harsh feedback · K=Onboarding gaps · L=Inconsistent visual language


1. Executive Summary

This week's audit covered every Swift file in the project. The codebase continues to mature: the March 2026 UX redesign has landed (MeetPipAnimated reduced to 3 dialogs, allergen skip affordance present, .trailingFade() applied to most horizontal scrolls), and the new multiplayer suite is well-engineered for a young audience. The main blockers before a 6+ ship are (1) free users receiving zero audio—pre-readers in this age group need AVSpeechSynthesizer at minimum, (2) several recipe step sequences that far exceed the 4-step cognitive limit, and (3) a cluster of sub-11pt labels scattered across game views. 14 actionable findings are documented below, ranked P0–P2. All recommended token additions follow the hard rule: no hardcoded colors, fonts, or spacing.


2. What's Working Well

# Area Observation
1 PipDialogView Auto-speaks on .onAppear; first-class voice integration
2 PlantingSheet Morph transition (matchedGeometryEffect) is delightful
3 RecipeDetailView Sticky "Let's Cook!" footer — never requires scrolling past steps
4 FarmShopView Info-first purchase flow; taps item → reads info → optional buy
5 Horizontal scrolls .trailingFade() applied consistently (Garden seed row, Farm shop)
6 BouncyButtonStyle Used broadly; gives every tap immediate tactile-like feedback
7 CookingCompletionView Sequential star reveal with spring animation — celebration peak
8 AskPipView Topic-grouped starter questions support pre-readers via icon groups
9 GiftVeggieSheet PipDialog confirm before irreversible gift action — no silent undo
10 ParentPINEntryView "Forgot PIN?" → Apple ID verification; recoverable without support
11 MeetPipAnimated Reduced from 7 to 3 dialogs — previous audit fix implemented
12 GardenWeatherService pipMessages are age-appropriate ("the plants are thirsty!")
13 AllergenPickerStep "None of these" skip + kid-friendly explanations on each allergen
14 PipAIService System prompt explicitly forbids clinical nutrition terms
15 InsulinTetrisView Bin hit-box expanded +20pt each side — forgiving for small hands
16 InsulinTetrisView Fiber block count derives from player's actual harvested garden veggies
17 GameCenterService Access point at .bottomTrailing — avoids top-corner tap conflicts
18 AssetPackController/ODRManager Pip waving animation shown during asset downloads — no blank loading
19 NearbyVersusView Radar circle animation during peer search — strong visual feedback
20 MultiplayerManager Error escalation reads "Ask a grown-up to turn on Game Center" — age-correct
21 SplitScreenVersusView / LocalVersusView Both players earn coins regardless of winner — no losing-child frustration
22 PipStaticResponses "What can I cook right now?" runs local game logic — no paywall, instant answer
23 SubscriptionManager Cached premium state prevents paywall flash on cold launch
24 WeatherOverlayView allowsHitTesting(false) — weather particles never steal garden taps
25 SeasonalOverlayView Particle positions seeded once in @State — no jitter on parent re-render

3. Findings

P0 — Ship Blockers


F-01 · [F] Free users receive zero voice feedback

File: PipVoice.swift.readText case in speak()
Impact: Pre-readers aged 6–7 are the primary audience. When voiceMode == .readText, PipVoice.shared.speak() returns silently. Every Pip dialogue, cooking instruction, and mini-game cue is text-only for non-subscribers. A child who cannot read is effectively locked out of the narrative layer of the game without their parent purchasing Pip Plus.

Recommendation: Add AVSpeechSynthesizer as the free-tier fallback inside the .readText branch. Use the system voice — no API cost, no subscription required. Pip Plus retains the ElevenLabs character voice as its exclusive premium benefit. This is the single highest-impact UX change for the target age group.

// PipVoice.swift — in speak()
case .readText:
    // Fallback: system voice so pre-readers can hear instructions
    let utterance = AVSpeechUtterance(string: text)
    utterance.rate = 0.48          // slightly slower than default for young listeners
    utterance.pitchMultiplier = 1.1
    synthesizer.speak(utterance)

No new token needed; this is logic only.


F-02 · [F] Recipe step count far exceeds 4-step cognitive limit

File: RecipeCardExample.swiftGardenRecipes.all
Impact: The March 2026 audit established a hard limit of 4 cooking steps for this age group. Current data:

  • "Garden Veggie Omelette" — 8 steps
  • "Chicken Veggie Skillet" — 11 steps
  • Multiple others exceed 4 steps

A 6-year-old reading 11 sequentially numbered instructions before the first mini-game will either scroll past the "Let's Cook!" button or hand the device back to a parent.

Recommendation: Cap each recipe at 4 steps displayed. Collapse additional steps under a "Show all steps" expander that defaults to collapsed. This is additive — parents and older siblings can still see the full recipe.

Token needed in AppTheme.swift:

// Add under AppSpacing:
static let maxRecipeStepsDisplayed: Int = 4

C-01 · [C/L] HomeImageMap tap targets have zero visual affordance

File: ChefAcademyApp.swiftHomeImageMap / HomeInteractiveTapLayer
Impact: Interactive hot-zones on the home map are implemented as Color.clear.contentShape(Rectangle()). Children have no indication that these regions are tappable. This is anti-pattern C (missing affordance) combined with L (inconsistent visual language — every other tappable element in the app has a visible button frame or bounce animation).

Recommendation: Add a subtle pulsing ring or a small animated arrow on each interactive zone when the screen first loads. Use BouncyButtonStyle() on the tap regions so touch produces the same feedback as all other buttons. If the art direction requires invisible zones, add a first-launch coach mark (using PipDialogView) that physically points at each zone.


L-01 · [L] Primary CTA color is muted goldenWheat — invisible against cream backgrounds

File: AppTheme.swiftPrimaryButtonStyle; MeetPipAnimated.swift, OnboardingView.swift
Impact: The paper/cream background palette (#FDF6E3) has low contrast with goldenWheat (#DAA520). A 6-year-old scanning the screen will not be drawn to the primary action. The March 2026 UX audit flagged this and recommended bright accent colors; PrimaryButtonStyle was not updated.

Recommendation: Add three new accent tokens to AppTheme.swift and update PrimaryButtonStyle to use the first:

// AppTheme.swift — add to Color.AppTheme extension
static let brightGreen   = Color(hex: "4CAF50")  // high-visibility CTA (WCAG AA on cream)
static let brightBlue    = Color(hex: "2196F3")  // secondary CTA / informational
static let sunflowerYellow = Color(hex: "FFD600") // reward / celebration accents

Update PrimaryButtonStyle to use Color.AppTheme.brightGreen as the fill. TexturedButtonStyle with .tint(Color.AppTheme.brightGreen) should be the default for all "Let's go!" / "Start!" / "Done!" buttons across the app.


P1 — High Priority


F-03 · [F] Multiple labels below 11pt minimum across game views

Files and exact locations:

View Element Size Fix
PlotView.swift Growth percentage label .rounded(size: 9) Raise to .rounded(size: 13)
GlucoseJourneyView.swift Diagram node labels 8–9pt inline Raise to AppTheme.caption (12pt)
BodyBuddyView.swift Organ name labels 9pt inline Raise to AppTheme.caption (12pt)
InsulinTetrisView.swift Bin capacity counter .rounded(size: 10) Raise to .rounded(size: 13)
InsulinTetrisView.swift Fat bin block counter .rounded(size: 10) Raise to .rounded(size: 13)
SplitScreenVersusView.swift Flying food name label .rounded(size: 9) Raise to .rounded(size: 12)

Children aged 6+ with typical visual acuity struggle to read below 11pt on a 6-inch phone at arm's reach. All game feedback labels must be readable during rapid gameplay.

Token to add if not present:

// AppTheme.swift
static var micro: Font { .AppTheme.rounded(size: 12, weight: .semibold) }

D-01 · [D] GardenModeButton tap target undersized

File: GardenHubView.swiftGardenModeButton
Impact: Mode toggle buttons use .padding(.vertical, 6) — approximate tap height ~34pt, below the 44pt minimum for ages 6+. Children navigating between Garden views will frequently mis-tap.

Recommendation: Change .padding(.vertical, 6) to .padding(.vertical, AppSpacing.sm) (which resolves to ≥10pt), giving a total tap height of ~44pt. No new token needed — AppSpacing.sm already exists.


K-01 · [K] Gender selection is binary (boy / girl only)

File: AvatarModel.swiftGender enum; also affects CharacterWalkingView.swift, SplitScreenVersusView.swift, LocalVersusView.swift, NearbyVersusView.swift, MultiplayerHealthyPicksView.swift
Impact: All five multiplayer views use child.gender == .boy ? "boy_card_..." : "girl_card_..." — a hard binary. Families with non-binary children will find their child's avatar defaulting to a gendered frame they did not choose. The March 2026 audit listed this as P1.

Recommendation:

// AvatarModel.swift
enum Gender: String, Codable, CaseIterable {
    case boy, girl, nonBinary
}

Add a nonBinary character frame set to CharacterFrameSet and map nonBinary → the existing girlWalking frames until dedicated assets are created. Update all binary ? "boy_..." : "girl_..." switches to three-way switch statements.


F-04 · [F] Scientific nutrient names in child-facing UI

Files: SeedInfoView.swift, PantryInfoView.swift, USDAFoodService.swift topNutrients()
Impact: topNutrients() passes nutrient names like "Lycopene", "Beta-carotene", and "Lutein" directly into display strings that appear on SeedInfoView and PantryInfoView. These words are not in a 6-year-old's vocabulary. PipAIService's system prompt explicitly bans clinical terms; the same standard must apply to static UI strings.

Recommendation: In USDAFoodService.topNutrients(), the name field is already separated from value/organ/emoji — simply replace the scientific names with the kid-friendly values already in the tuple:

// Current: results.append(("Lycopene", "Red power!", "Heart", "❤️"))
// The display label should use "Red power!" not "Lycopene"
// Update the tuple order so callers use index 1 (kid label) not index 0 (scientific)

No token change needed; this is a data ordering fix.


C-02 · [C] Gesture-only instructions are text-only with no visual demo

Files: PlotView.swift ("Swipe up to harvest!"), CookingMiniGames.swift (StirMiniGame "Stir in circles!", PeelMiniGame "Swipe down to peel!")
Impact: Children who cannot read the gesture instruction have no visual model for what to do. First encounters with each mini-game gesture will result in random tapping until accidental discovery — frustrating and opaque for a 6-year-old.

Recommendation: Add a one-shot gesture hint animation on first appearance: a translucent finger icon that mimes the expected gesture (arc for stir, downward swipe for peel, upward swipe for harvest). Store "has seen hint" in UserDefaults per mini-game type so it only shows once. Use AnimationConstants.pipTransition timing.


G-01 · [G] SiblingProfileView displays raw recipeID string

File: SiblingProfileView.swift — favorite recipe display
Impact: profile.favoriteRecipeID is rendered directly as a string when no matching recipe title is found. A child sees a UUID-style identifier like "chicken-veggie-platter" instead of a recipe name. Even the slug form is confusing to a non-reader.

Recommendation: Look up the recipe by ID in GardenRecipes.all before display. Fall back to "No favourite yet 🍴" rather than the raw ID:

let title = GardenRecipes.all.first { $0.id == profile.favoriteRecipeID }?.title ?? "No favourite yet 🍴"

L-02 · [L] Hardcoded colors in GardenWeatherService and WeatherOverlayView

File: GardenWeatherService.swiftGardenWeather.iconColor; WeatherOverlayView.swiftSunshineOverlay, PartlyCloudyOverlay, CloudOverlay, RainOverlay, StormOverlay, SnowOverlay
Impact: iconColor uses .yellow, .orange, .gray, .blue, .purple, .cyan (raw SwiftUI named colors). GardenSeason.gradientColors uses Color(hex: "E8F5E9") hex literals. WeatherOverlayView particle renderers repeat .blue.opacity(0.4), .yellow.opacity(0.3), .gray.opacity(0.3), .cyan.opacity(0.05), .white.opacity(0.7), .pink.opacity(0.35). These will not respect any future dark-mode adaptation and violate the token rule.

Recommendation — tokens to add in AppTheme.swift:

// Weather icon colors
static let weatherSunny       = Color(hex: "F9A825")  // warm amber
static let weatherCloudy      = Color(hex: "90A4AE")  // blue-grey
static let weatherRainy       = Color(hex: "5C9BD6")  // sky blue
static let weatherStormy      = Color(hex: "7E57C2")  // storm purple
static let weatherSnowy       = Color(hex: "81D4FA")  // ice blue
static let weatherPartlyCloudy = Color(hex: "FB8C00") // warm orange

// Season gradient tokens (replace Color(hex:) literals)
static let springGradientTop  = Color(hex: "C8E6C9")
static let springGradientBot  = Color(hex: "F8BBD9")
static let summerGradientTop  = Color(hex: "FFF9C4")
static let summerGradientBot  = Color(hex: "FFCC80")
static let fallGradientTop    = Color(hex: "FFE0B2")
static let fallGradientBot    = Color(hex: "D7CCC8")
static let winterGradientTop  = Color(hex: "E3F2FD")
static let winterGradientBot  = Color(hex: "E8EAF6")

Replace all raw .yellow, .blue, .gray, .cyan, .purple, .pink, .orange, .white color literals in both files with the tokens above or with existing Color.AppTheme.* tokens where appropriate (e.g. Color.AppTheme.sage already exists for wind streaks — correctly used).


L-03 · [L] Mini-game CTAs use inline fill instead of TexturedButtonStyle

Files: HealthyChoiceGameView.swift, InsulinTetrisView.swift, LocalVersusView.swift, NearbyVersusView.swift, SplitScreenVersusView.swift
Impact: "Let's Go!", "Try Again!", "Play Again!", "Start!", "Rematch!" all use manual .background(Color.AppTheme.sage).cornerRadius(...) patterns. This diverges from the standard .buttonStyle(TexturedButtonStyle(tint:)) used in RecipeDetailView, FarmShopView, and CookingSessionView. The inconsistency means some buttons feel physically pressable (textured) and others feel flat — disrupting the tactile metaphor for young players.

Recommendation: Replace all manual background + cornerRadius CTA patterns in the five files above with:

.buttonStyle(TexturedButtonStyle(tint: Color.AppTheme.sage))   // or appropriate tint

P2 — Medium Priority


I-01 · [I] CookTimerMiniGame "Done!" button disabled until 35% elapsed

File: CookingMiniGames.swiftCookTimerMiniGame
Impact: The green-zone timer only activates "Done!" after 35% has elapsed. If a child taps too early (before the zone), there is no feedback explaining why the button is greyed out — it simply does not respond. For a 6-year-old this reads as "broken."

Recommendation: Add a brief PipSpeechBubble cue when a pre-zone tap is detected: "Wait for the green zone!" This costs one PipVoice.shared.speak() call and reuses the existing speech bubble component.


H-01 · [H] HomeAnimated PipMessageAnimated does not call PipVoice

File: HomeAnimated.swiftPipMessageAnimated
Impact: When the rotating Pip message changes (every ~4 seconds), only the text updates — PipVoice.shared.speak() is never called. Every other PipSpeechBubble appearance in the app speaks on .onAppear. Pre-readers on the Home screen receive no audio even if they have Pip Plus.

Recommendation: In the onChange(of: messageIndex) handler (or equivalent animation completion), call:

PipVoice.shared.speak(messages[messageIndex])

H-02 · [H] HealthyChoiceGameView presents 4 multiplayer modes simultaneously

File: HealthyChoiceGameView.swift — ready screen
Impact: The mode-selection screen shows "Solo", "Split Screen", "Take Turns", and "Online Battle" as four equally-weighted buttons. Hick's Law: reaction time increases logarithmically with the number of choices. For a 6-year-old, four peer-level options cause choice paralysis. The most common mode (Solo) is not visually distinguished from less-used online features.

Recommendation: Default to Solo play with a collapsed "Play with friends ▾" section that reveals the three multiplayer modes. This reduces the initial decision to 2 choices while keeping all modes accessible.


H-03 · [H] VoicePickerView does not mention that "Read Text" is still spoken

File: VoicePickerView.swiftVoiceOptionCard for .readText
Impact: After the F-01 fix (adding AVSpeechSynthesizer to the free tier), the subtitle "Read Pip's words on screen — no voice" will be inaccurate. Free users will hear a system voice but the UI says they won't.

Recommendation: Update the subtitle to "Pip's words + system voice — free" once F-01 is implemented. This is a content fix, not a code change; flagged here so it is not overlooked.


4. Hardcoded Token Violations

Files that contain hardcoded colors, fonts, or spacing that must be migrated before any App Store submission:

File Violation Replacement
GardenWeatherService.swift:iconColor .yellow, .orange, .gray, .blue, .purple, .cyan New weather tokens (see L-02)
GardenWeatherService.swift:gradientColors Color(hex: "E8F5E9") etc. New season gradient tokens (see L-02)
WeatherOverlayView.swift:SunshineOverlay Color.yellow.opacity(0.3), Color.orange.opacity(0.1) Color.AppTheme.weatherSunny
WeatherOverlayView.swift:PartlyCloudyOverlay Color.yellow.opacity(0.2), Color.white.opacity(0.6) Color.AppTheme.weatherPartlyCloudy, Color.AppTheme.cream
WeatherOverlayView.swift:CloudOverlay Color.gray.opacity(0.3), Color.gray.opacity(0.25) Color.AppTheme.weatherCloudy
WeatherOverlayView.swift:RainOverlay Color.blue.opacity(0.4) Color.AppTheme.weatherRainy
WeatherOverlayView.swift:StormOverlay Color.blue.opacity(0.5) Color.AppTheme.weatherRainy
WeatherOverlayView.swift:SnowOverlay Color.cyan.opacity(0.05), Color.white.opacity(0.7) Color.AppTheme.weatherSnowy
WeatherOverlayView.swift:SpringParticles Color.pink.opacity(0.35) New token: Color.AppTheme.springPetal
SceneEditor.swift .yellow, .cyan, .green, .red Dev tool — no user-facing impact; exempt

SceneEditor.swift is a dev-only tool with no user-facing rendering and is explicitly exempt from the token rule.


5. Tokens to Add to AppTheme.swift

All tokens needed by the findings above, grouped for a single PR:

// MARK: - Weather Colors (add to Color.AppTheme extension)
static let weatherSunny        = Color(hex: "F9A825")
static let weatherPartlyCloudy = Color(hex: "FB8C00")
static let weatherCloudy       = Color(hex: "90A4AE")
static let weatherRainy        = Color(hex: "5C9BD6")
static let weatherStormy       = Color(hex: "7E57C2")
static let weatherSnowy        = Color(hex: "81D4FA")

// MARK: - Season Gradient Colors
static let springGradientTop   = Color(hex: "C8E6C9")
static let springGradientBot   = Color(hex: "F8BBD9")
static let summerGradientTop   = Color(hex: "FFF9C4")
static let summerGradientBot   = Color(hex: "FFCC80")
static let fallGradientTop     = Color(hex: "FFE0B2")
static let fallGradientBot     = Color(hex: "D7CCC8")
static let winterGradientTop   = Color(hex: "E3F2FD")
static let winterGradientBot   = Color(hex: "E8EAF6")

// MARK: - CTA Accent Colors (from March 2026 audit — now critical)
static let brightGreen         = Color(hex: "4CAF50")
static let brightBlue          = Color(hex: "2196F3")
static let sunflowerYellow     = Color(hex: "FFD600")

// MARK: - Spring Particle
static let springPetal         = Color(hex: "F48FB1")  // soft pink

// MARK: - AppSpacing
static let maxRecipeStepsDisplayed: Int = 4

// MARK: - Font
static var micro: Font { .AppTheme.rounded(size: 12, weight: .semibold) }

6. Priority Summary

ID Priority Category File(s) Description
F-01 P0 F PipVoice.swift Add AVSpeechSynthesizer free-tier fallback
F-02 P0 F RecipeCardExample.swift Cap recipe steps at 4 + "Show more" expander
C-01 P0 C/L ChefAcademyApp.swift Add visual affordances to HomeImageMap tap zones
L-01 P0 L AppTheme.swift, PrimaryButtonStyle Add brightGreen/brightBlue/sunflowerYellow tokens; update CTAs
F-03 P1 F PlotView, GlucoseJourneyView, BodyBuddyView, InsulinTetrisView, SplitScreenVersusView Fix 5 sub-11pt label sizes
D-01 P1 D GardenHubView.swift GardenModeButton vertical padding → AppSpacing.sm
K-01 P1 K AvatarModel.swift + 5 multiplayer files Add non-binary gender option
F-04 P1 F USDAFoodService.swift, SeedInfoView, PantryInfoView Replace scientific nutrient names with kid-friendly labels
C-02 P1 C PlotView, CookingMiniGames Add gesture demo animations on first mini-game encounter
G-01 P1 G SiblingProfileView.swift Show recipe title instead of raw recipeID
L-02 P1 L GardenWeatherService, WeatherOverlayView Replace hardcoded color literals with AppTheme tokens
L-03 P1 L 5 game view files Replace manual background+cornerRadius CTAs with TexturedButtonStyle
I-01 P2 I CookingMiniGames.swift Add PipSpeechBubble cue when pre-zone tap detected
H-01 P2 H HomeAnimated.swift Call PipVoice.speak() on PipMessageAnimated change
H-02 P2 H HealthyChoiceGameView.swift Collapse 4 multiplayer modes into Solo + expandable "Play with friends"
H-03 P2 H VoicePickerView.swift Update "Read Text" subtitle after F-01 AVSpeechSynthesizer fix

7. Files With No UX Issues

The following files were read and found to have no child-facing UX concerns (infrastructure, dev tools, or correctly implemented):

AmbientAudioPlayer.swift, AppAttestService.swift, AssetPackController.swift, AssetPackImage.swift, AuthManager.swift, BackgroundView.swift, CloudKeyManager.swift, ContentView.swift, ElevenLabsVoiceService.swift, GameCenterMatchmakerView.swift, GameCenterService.swift, MorphTransition.swift, NearbyMultiplayerManager.swift, MultiplayerManager.swift, ODRManager.swift (deprecated, kept for compatibility), PINKeychain.swift, PipFoundationModelService.swift, PipGameAnimationView.swift, PipStaticResponses.swift, PipTestView.swift (dev tool), SceneEditor.swift (dev tool), SeededRandomGenerator.swift, SessionManager.swift, SubscriptionManager.swift, USDAFoodService.swift (data layer), VideoPlayerView.swift, WorkerClient.swift


8. Regression Check — Previous Audit Items

Previous Finding Status
MeetPipAnimated: 7 dialogs → 3 ✅ Fixed (MeetPipAnimated.swift confirmed 3-dialog flow)
AllergenPickerStep: add "None of these" skip ✅ Fixed (button present, correctly styled)
FarmShopView: info-first purchase flow ✅ Implemented
.trailingFade() on horizontal scrolls ✅ Applied to Garden seed row and Farm shop
PipDialogView auto-speak ✅ Present
P0: AVSpeechSynthesizer free tier ❌ Not yet implemented (F-01, remains P0)
P0: Brighten CTA palette ❌ Not yet implemented (L-01, remains P0)
P1: Non-binary gender option ❌ Not yet implemented (K-01)
P1: Cap recipe steps at 4 ❌ Not yet implemented (F-02, escalated to P0)
P1: Adult nutrient names in child UI ❌ Not yet implemented (F-04)

Generated by automated weekly audit — 2026-05-11