Cross-platform refinements to appearance/battery/charger editors, tabs and navigation, plus persistence, screenshot previews and CLAUDE.md docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Two apps in this repo
- iOS (
Cable/) — SwiftUI + SwiftData. This document describes the iOS app unless stated otherwise. - Android (
android/) — a native Kotlin/Jetpack Compose port that mirrors every iOS feature (package rootapp.voltplan.cable, Room persistence, same Aptabase analytics). Seeandroid/README.mdfor its architecture and build instructions before working on it.
Behavior and data shape are meant to stay in sync across both — when changing a user-facing feature on one platform, check the other. See Export Options for one such cross-platform contract.
Apply every instruction to both the iOS and Android versions unless explicitly told otherwise. Any feature, fix, or change requested without naming a platform must land on both apps and stay behaviorally in sync.
Build & Test Commands
Use the Xcode MCP server tools instead of xcodebuild CLI:
- List windows first: Always run
mcp__xcode__XcodeListWindowsto find the correcttabIdentifier— 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) ormcp__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 parametersSavedBattery— battery banks with chemistry-specific capacity rulesSavedCharger— 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:
- Configuration struct (e.g.
BatteryConfiguration,ChargerConfiguration) as a transient draft — not an@Model. Has dual inits: one for new items, one from an existingSaved*model. - Alert-based field editing: Each numeric field gets its own
.alert()with aTextField, bound via anEditingFieldenum.onChangeupdates the configuration live as the user types. - Snap-to-values: Common electrical values (12V, 24V, 48V, 230V, etc.) snap within a tolerance when the field is not actively being edited.
- Save flow:
onSave: (Configuration) -> Voidcallback to the parent view.onDisappeartriggers save automatically. Parent callsSystemComponentsPersistenceto persist. - Appearance modal: All editors include a
.sheetpresentingItemEditorViewfor 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). UsesAutoSelectTextFieldfor auto-focus. Accepts optionaladditionalFieldsbuilder.StatsHeaderContainer(StatsHeaderContainer.swift): Header card with scrollable metric pills. UsesglassEffecton 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 andIconCache(memory + file-based). Also exportsComponentSummaryMetricViewandComponentMetricBadgeView.
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)containingStatsHeaderContainer
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
UpperCamelCasefor types,lowerCamelCasefor 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:)— notNSLocalizedString. ThedefaultValueserves 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.
Export Options
Three export options, available from the Overview tab's share menu plus the BOM sheet. Keep iOS and Android (android/.../pdf/) in sync — they must offer the same exports.
- System Overview (PDF) — summary + a full-page wiring diagram + per-entity tables.
- Bill of Materials (PDF) — categorized component list.
- Wiring Diagram (PNG) — standalone diagram image.
The wiring diagram (used both as the standalone PNG and the Overview PDF's diagram page) is fetched from the shared VoltPlan diagram API (POST https://voltplan.app/api/diagram/generate, JSON payload of system/loads/batteries/chargers, returns PNG). Both platforms send the identical payload shape; falls back gracefully when the API is unreachable (iOS draws a Core Graphics diagram; Android omits the PDF page / shows an error toast for the standalone export).
PDF Export Pattern (iOS)
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
@Stateitem binding in the parent view. - Toolbar button (not inline content) for the export action.
On Android the equivalents live in android/app/src/main/java/app/voltplan/cable/pdf/: SystemOverviewPdf, SystemBomPdf, SystemDiagram (diagram fetch + PNG export), and PdfShare (PdfDocument + FileProvider/ACTION_SEND).
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
shooter.shreadsscreenshot.configfor scheme, bundle ID, devices, and languages.- Builds once with
build-for-testing, then runstest-without-buildingper language. - 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). xcparseextracts screenshot attachments from.xcresultbundles intoShots/Screenshots/{device-slug}/{lang}/.- 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-datalaunch 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
viewModelviainit(viewModel:onSelect:). UseComponentLibraryViewModel(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 inproject.pbxproj, resolved automatically by Xcode. - CocoaPods:
Podfileexists but has no pods listed — effectively unused. Package.resolvedis inxcshareddatawhich 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.