- Fix AWG 0/00/000/0000 bug (all resolved to 0 in Swift) using negative int convention (-1 through -4) with formatAWG() for 1/0–4/0 display - Add 7.5A fuse size and change fuse type from Int to Double - Add alternator power source type with distinct bolt.car.fill icon - Migrate all NSLocalizedString calls to String(localized:defaultValue:) - Update translations for runtime subtitle (ES/FR/NL: current→maximum), usable capacity footer text, and NL override wording - Store length always in meters, convert at display time in CalculatorView - Add preview-friendly inits for ComponentLibraryView and LoadsView - Expand test coverage for calculations, fuses, AWG, and edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.7 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__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.
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
@Stateitem binding in the parent view. - Toolbar button (not inline content) for the export action.
Screenshots & Previews
Screenshot previews for all major views live in Cable/ScreenshotPreviews.swift. This file contains wrapper views and realistic sample data so screenshots include full app chrome (NavigationBar, TabBar).
How to render screenshots
- Use
mcp__xcode__RenderPreviewwithsourceFilePath: "Cable/ScreenshotPreviews.swift"andpreviewDefinitionIndexInFile(0–44, grouped by view × 5 languages). - Output goes to
Shots/Screenshots/.
Preview index mapping
Previews are grouped in blocks of 5 (EN, DE, ES, FR, NL):
- 0–4: Overview tab
- 5–9: Components tab
- 10–14: Batteries tab
- 15–19: Chargers tab
- 20–24: Systems list
- 25–29: Parts Library
- 30–34: Load editor (CalculatorView)
- 35–39: Battery editor
- 40–44: Charger editor
Key patterns for preview-friendly views
- LoadsView accepts an
initialTabparameter (LoadsView.ComponentTab) to control which tab is shown. Wrap inNavigationStackto get the navigation bar. - ComponentLibraryView accepts an optional
viewModelparameter viainit(viewModel:onSelect:). UseComponentLibraryViewModel(previewItems:)to inject mock data and avoid network dependency. - All preview wrappers inject
.modelContainer(container)(in-memory) and.environmentObject(UnitSystemSettings()).
Localization limitation
.environment(\.locale, Locale(identifier: "xx")) does not affect String(localized:defaultValue:) — those resolve from the app bundle, not the SwiftUI environment. All preview screenshots render in the system language. For multi-language screenshots, use UI tests with -AppleLanguages launch arguments or change the Xcode scheme language.
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.