Files
Cable/CLAUDE.md
Stefan Lange-Hegermann b448a1b4f7 Automate App Store screenshots via XCUITests
Replace preview-based screenshot rendering with simulator XCUITests
driven by shooter.sh (per-language locale + status bar overrides,
xcparse extraction). Add batteries-list accessibility identifier and
update CLAUDE.md screenshot docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:39:46 +02:00

11 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Build & Test Commands

Use the Xcode MCP server tools instead of xcodebuild CLI:

  • List windows first: Always run mcp__xcode__XcodeListWindows to find the correct tabIdentifier — it changes depending on which Xcode windows are open.
  • Build: mcp__xcode__BuildProject
  • Run all tests: mcp__xcode__RunAllTests
  • Check errors: mcp__xcode__GetBuildLog (severity: error) or mcp__xcode__XcodeListNavigatorIssues

No external dependencies beyond the Xcode toolchain.

Architecture

SwiftUI + SwiftData app for sizing low-voltage electrical conductors (boats, RVs, off-grid).

Data Model Hierarchy

ElectricalSystem is the top-level container. Child entities reference their parent via an optional system: ElectricalSystem? property:

  • SavedLoad — individual electrical loads with wire sizing parameters
  • SavedBattery — battery banks with chemistry-specific capacity rules
  • SavedCharger — charging equipment specs

All are @Model classes persisted via SwiftData. The container is configured in CableApp.swift and injected into the SwiftUI environment.

Key Layers

  • Calculation engine (Loads/ElectricalCalculations.swift): Pure static functions for wire cross-section sizing, voltage drop, power loss, and fuse recommendations. Uses copper resistivity (0.017 Ω·mm²/m) with a 5% max voltage drop constraint. Supports both metric (mm²) and imperial (AWG) wire standards.
  • CableCalculator (Loads/CableCalculator.swift): ObservableObject wrapper that bridges the calculation engine to SwiftUI views. Instantiated per-view as @StateObject, not globally injected.
  • App-wide state: UnitSystemSettings (metric/imperial, persisted to UserDefaults) is injected as @EnvironmentObject.

Navigation Flow

SystemsView (root list) → LoadsView (per-system TabView with 4 tabs: Overview, Components, Batteries, Chargers) → individual editor modals for each entity type.

Feature Organization

Each feature has its own directory under Cable/: Loads/, Systems/, Batteries/, Chargers/, Overview/, Shared/. Models and views for each feature live together.

SwiftData Query Pattern

@Query fetches all records globally, then a computed property filters by system relationship — SwiftData doesn't efficiently handle relationship-based predicates in @Query:

@Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad]
private var savedLoads: [SavedLoad] { allLoads.filter { $0.system == system } }

Unit Storage Convention

All values are stored in metric (meters, mm²). Conversion to imperial (feet, AWG) happens at display time only, never at storage time. UnitSystemSettings.unitSystem drives the display format.

UI Patterns

Editor Pattern (Battery, Charger, Load)

All editors follow the same architecture:

  1. Configuration struct (e.g. BatteryConfiguration, ChargerConfiguration) as a transient draft — not an @Model. Has dual inits: one for new items, one from an existing Saved* model.
  2. Alert-based field editing: Each numeric field gets its own .alert() with a TextField, bound via an EditingField enum. onChange updates the configuration live as the user types.
  3. Snap-to-values: Common electrical values (12V, 24V, 48V, 230V, etc.) snap within a tolerance when the field is not actively being edited.
  4. Save flow: onSave: (Configuration) -> Void callback to the parent view. onDisappear triggers save automatically. Parent calls SystemComponentsPersistence to persist.
  5. Appearance modal: All editors include a .sheet presenting ItemEditorView for name/icon/color editing.

Navigation to editors uses .navigationDestination(item: $draft) where $draft is a @State optional Configuration.

Shared UI Components

  • ItemEditorView (ItemEditorView.swift): Shared appearance editor for name, icon (5-column SF Symbol grid), and color (6-column grid). Uses AutoSelectTextField for auto-focus. Accepts optional additionalFields builder.
  • StatsHeaderContainer (StatsHeaderContainer.swift): Header card with scrollable metric pills. Uses glassEffect on iOS 26+, fallback with colored background + border.
  • OnboardingInfoView (Loads/OnboardingInfoView.swift): Empty state with auto-rotating image carousel (8s interval). Configured via static factory methods: .loads(), .battery(), .charger().
  • LoadIconView (Loads/LoadIconView.swift): Component icon with remote URL support and IconCache (memory + file-based). Also exports ComponentSummaryMetricView and ComponentMetricBadgeView.

List Styling Convention

All list views use consistent styling:

  • .listStyle(.plain), .scrollContentBackground(.hidden), .scrollIndicators(.hidden)
  • .listRowSeparator(.hidden), .listRowBackground(Color.clear)
  • Row cards: RoundedRectangle(cornerRadius: 18, style: .continuous).fill(Color(.systemBackground)) with 16pt padding
  • Headers via .safeAreaInset(edge: .top) containing StatsHeaderContainer

Color Convention

Color.componentColor(named:) (in LoadIconView.swift) maps string names to SwiftUI colors (13 options: blue, green, orange, red, purple, yellow, pink, teal, indigo, mint, cyan, brown, gray). Used for all component icons. Badge backgrounds use tint.opacity(0.12).

Analytics

AnalyticsTracker.log(_:properties:) wraps Aptabase. Tracks: create, edit, update, delete, tab changes, navigation. In DEBUG builds, prints to NSLog.

Component Library

ComponentLibraryViewModel fetches from a PocketBase backend with paginated API calls. Items have multi-language translations and locale-aware affiliate links (exact region match → language-only fallback → global fallback). Use ComponentLibraryViewModel(previewItems:) to bypass network in previews.

Code Style

  • 4-space indentation, trailing commas on multiline collections, 120-char soft line limit
  • UpperCamelCase for types, lowerCamelCase for methods/properties
  • Test naming: testScenario_expectedResult
  • Commit messages: short imperative subjects under 50 characters

Localization

5 languages: English (base), German, Spanish, French, Dutch. Translation files are in *.lproj/Localizable.strings and Localizable.stringsdict.

  • Use String(localized:defaultValue:)not NSLocalizedString. The defaultValue serves as English fallback and avoids showing raw keys when a Localizable.strings entry is missing.
  • When adding new user-facing strings, add translations to all 5 Localizable.strings files immediately.

PDF Export Pattern

PDF exports use UIGraphicsPDFRenderer with A4 portrait format. The pattern is:

  • Exporter struct (e.g. SystemOverviewPDFExporter, SystemBillOfMaterialsPDFExporter) with snapshot data types — keeps Core Graphics rendering isolated from SwiftUI/SwiftData.
  • ShareSheet triggered via @State item binding in the parent view.
  • Toolbar button (not inline content) for the export action.

Screenshots

App Store screenshots are generated via XCUITests running on simulators, automated by shooter.sh.

Running screenshots

./shooter.sh                    # reads ./screenshot.config
./shooter.sh other.config       # explicit config file
VERBOSE=1 ./shooter.sh          # full logs on failure
PARALLEL=0 ./shooter.sh         # sequential mode

Requires xcparse: brew install chargepoint/xcparse/xcparse.

How it works

  1. shooter.sh reads screenshot.config for scheme, bundle ID, devices, and languages.
  2. Builds once with build-for-testing, then runs test-without-building per language.
  3. Per language run: erases simulator, sets locale via simctl spawn ... defaults write, suppresses system notifications (DND, Apple Intelligence), overrides status bar (9:41, full battery).
  4. xcparse extracts screenshot attachments from .xcresult bundles into Shots/Screenshots/{device-slug}/{lang}/.
  5. Devices run in parallel (languages sequential per device — same simulator).

Test structure

  • Test target: CableUITestsScreenshot (scheme: CableScreenshots, test plan: CableScreenshots.xctestplan)
  • Test file: CableUITestsScreenshot/CableUITestsScreenshot.swift
  • Sample data: UITestSampleData.swift — seeded via --uitest-sample-data launch argument, cleared via --uitest-reset-data

Screenshot inventory

# Name Source
01 OnboardingSystemsView Empty state after reset
02 OnboardingSystemView New system overview
03 LoadEditorView CalculatorView for new load
04 ComponentSelectorView Component library (network)
05 SystemsWithSampleData Systems list with sample data
06 AdventureVanOverview Overview tab
07 AdventureVanLoads Components tab
08 BillOfMaterials System BOM sheet
09 AdventureVanCalculator Load calculator
10 AdventureVanBatteries Batteries tab
11 BatteryEditor Battery editor
12 AdventureVanChargers Chargers tab
13 ChargerEditor Charger editor

Accessibility identifiers for UI tests

Key identifiers used by the screenshot tests: create-system-button, systems-list, system-overview, overview-tab, components-tab, batteries-tab, chargers-tab, loads-list, batteries-list, chargers-list, system-bom-button, system-bom-close-button, library-view-close-button, create-component-button, select-component-button.

Preview-friendly view inits

  • LoadsView accepts initialTab (LoadsView.ComponentTab) to control which tab is shown.
  • ComponentLibraryView accepts an optional viewModel via init(viewModel:onSelect:). Use ComponentLibraryViewModel(previewItems:) to bypass network.

Model Definitions

SwiftData @Model classes live in their feature directories: ElectricalSystem and SavedLoad in Loads/CableCalculator.swift, SavedBattery in Batteries/SavedBattery.swift, SavedCharger in Chargers/SavedCharger.swift. Item.swift is a legacy model.

Xcode Project Structure

The project uses automatic file inclusion — no explicit file references in project.pbxproj for Swift sources. New .swift files in the Cable/ directory are picked up automatically.

Dependencies

  • SPM: aptabase-swift (analytics) — referenced in project.pbxproj, resolved automatically by Xcode.
  • CocoaPods: Podfile exists but has no pods listed — effectively unused.
  • Package.resolved is in xcshareddata which is gitignored — it will be regenerated on a fresh checkout.

SourceKit Diagnostics

SourceKit may show false-positive errors (e.g. "Cannot find type in scope") while the project builds successfully. These are indexing artifacts — always verify with an actual build via mcp__xcode__BuildProject.