navigation fixed

This commit is contained in:
Stefan Lange-Hegermann
2025-09-16 22:00:04 +02:00
parent 177d5c350e
commit e081ca8b3b
5 changed files with 433 additions and 215 deletions

16
AGENTS.md Normal file
View File

@@ -0,0 +1,16 @@
# Repository Guidelines
## Project Structure & Module Organization
The SwiftUI application lives in `Cable/`, with `CableApp.swift` bootstrapping shared state and the scene hierarchy. Feature views such as `CalculatorView`, `SystemEditorView`, and `LoadEditorView` sit beside their supporting models (`CableCalculator`, `UnitSystem`, `Item`). Shared assets are stored in `Assets.xcassets`, while entitlement and configuration files remain at the top level. Unit tests reside in `CableTests/` and UI automation in `CableUITests/`; mirror new feature code with corresponding test folders.
## Build, Test, and Development Commands
Work in Xcode 15 or newer targeting the iOS 17 simulator. Use `open Cable.xcodeproj` to launch the project with the correct scheme configuration. From the command line, `xcodebuild -scheme Cable -destination 'platform=iOS Simulator,name=iPhone 15' build` performs a debug build, and `xcodebuild -scheme Cable -destination 'platform=iOS Simulator,name=iPhone 15' test` runs both unit and UI test bundles. Prefer running through Xcode when iterating on UI so previews and Instruments remain available.
## Coding Style & Naming Conventions
Follow idiomatic Swift style: four-space indentation, trailing commas for multiline collections, and 120-character soft line limits. Types use `UpperCamelCase`, methods and properties use `lowerCamelCase`, and tests follow `testScenario_expectedResult`. Keep SwiftUI view files focused on a single view struct, extracting supporting types or extensions into adjacent files when logic grows. Rely on Xcode's "Re-Indent" and "Add Missing Conformance" tools for quick formatting passes before reviews.
## Testing Guidelines
`CableTests.swift` demonstrates standard XCTest usage; add focused test cases that cover each calculator branch and unit conversion rule you touch. Place UI flows that require navigation assertions in `CableUITests/`, reusing launch helpers from `CableUITestsLaunchTests`. Run `xcodebuild ... test` (above) or the Test action in Xcode before submitting changes. Aim to keep or raise coverage for core calculation logic when introducing new units or system behaviors.
## Commit & Pull Request Guidelines
Existing history favors short, descriptive subjects (e.g., `systems first`); continue with concise, imperative summaries under 50 characters, adding optional bodies when context is needed. Group unrelated work into separate commits to keep diffs reviewable. Pull requests should link related issues, summarize functional changes, call out impacted screens, and attach simulator screenshots for major UI updates. Mention any follow-up tasks or technical debt explicitly in the PR description.

View File

@@ -30,8 +30,22 @@ struct SystemsView: View {
@Environment(\.modelContext) private var modelContext @Environment(\.modelContext) private var modelContext
@EnvironmentObject var unitSettings: UnitSystemSettings @EnvironmentObject var unitSettings: UnitSystemSettings
@Query(sort: \ElectricalSystem.timestamp, order: .reverse) private var systems: [ElectricalSystem] @Query(sort: \ElectricalSystem.timestamp, order: .reverse) private var systems: [ElectricalSystem]
@State private var newSystemToEdit: ElectricalSystem? @State private var systemNavigationTarget: SystemNavigationTarget?
@State private var systemToEdit: ElectricalSystem?
private struct SystemNavigationTarget: Identifiable, Hashable {
let id = UUID()
let system: ElectricalSystem
let presentSystemEditor: Bool
let loadToOpenOnAppear: SavedLoad?
static func == (lhs: SystemNavigationTarget, rhs: SystemNavigationTarget) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@@ -52,9 +66,6 @@ struct SystemsView: View {
.font(.title3) .font(.title3)
.foregroundColor(.white) .foregroundColor(.white)
} }
.onTapGesture {
systemToEdit = system
}
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(system.name) Text(system.name)
@@ -93,27 +104,11 @@ struct SystemsView: View {
} }
} }
} }
.navigationDestination(item: $newSystemToEdit) { system in .navigationDestination(item: $systemNavigationTarget) { target in
LoadsView(system: system) LoadsView(
} system: target.system,
.sheet(item: $systemToEdit) { system in presentSystemEditorOnAppear: target.presentSystemEditor,
SystemEditorView( loadToOpenOnAppear: target.loadToOpenOnAppear
systemName: Binding(
get: { system.name },
set: { system.name = $0 }
),
location: Binding(
get: { system.location },
set: { system.location = $0 }
),
iconName: Binding(
get: { system.iconName },
set: { system.iconName = $0 }
),
colorName: Binding(
get: { system.colorName },
set: { system.colorName = $0 }
)
) )
} }
} }
@@ -129,49 +124,113 @@ struct SystemsView: View {
.fill(Color.blue.opacity(0.1)) .fill(Color.blue.opacity(0.1))
.frame(width: 80, height: 80) .frame(width: 80, height: 80)
Image(systemName: "building.2") Image(systemName: "bolt.circle")
.font(.system(size: 40)) .font(.system(size: 40))
.foregroundColor(.blue) .foregroundColor(.blue)
} }
VStack(spacing: 8) { VStack(spacing: 8) {
Text("No Systems Yet") Text("Welcome to Cable by VoltPlan")
.font(.title2) .font(.title2)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Create your first electrical system to start managing loads and calculations.") Text("We'll create your first system and component so you can jump straight into the calculator.")
.font(.body) .font(.body)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, 32) .padding(.horizontal, 32)
} }
Button(action: { VStack(spacing: 12) {
createNewSystem() Button(action: {
}) { startComponentOnboarding()
HStack(spacing: 8) { }) {
Image(systemName: "plus.circle.fill") HStack(spacing: 8) {
.font(.system(size: 16)) Image(systemName: "plus.circle.fill")
Text("Create System") .font(.system(size: 16))
.fontWeight(.medium) Text("Create Component")
.fontWeight(.medium)
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue)
.cornerRadius(12)
} }
.foregroundColor(.white) .buttonStyle(.plain)
.frame(maxWidth: .infinity)
.frame(height: 50) Button(action: {
.background(Color.blue) // TODO: Open VoltPlan component library
.cornerRadius(12) print("Opening VoltPlan component library...")
}) {
HStack(spacing: 8) {
Image(systemName: "square.grid.3x3")
.font(.system(size: 16))
Text("Browse VoltPlan Library")
.fontWeight(.medium)
Image(systemName: "arrow.up.right")
.font(.system(size: 12))
}
.foregroundColor(.blue)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue.opacity(0.1))
.cornerRadius(12)
}
.buttonStyle(.plain)
} }
.buttonStyle(.plain)
.padding(.horizontal, 32) .padding(.horizontal, 32)
} }
Spacer() Spacer()
Spacer()
VStack(spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.font(.system(size: 16))
Text("Important Safety Notice")
.font(.headline)
.fontWeight(.semibold)
}
Text("This app provides estimates for educational purposes only. Always consult qualified electricians and follow local electrical codes for actual installations. Electrical work can be dangerous and should only be performed by licensed professionals.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
}
.padding(.bottom, 32)
} }
} }
private func createNewSystem() { private func createNewSystem() {
let system = makeSystem()
navigateToSystem(system, presentSystemEditor: true, loadToOpen: nil)
}
private func navigateToSystem(_ system: ElectricalSystem, presentSystemEditor: Bool, loadToOpen: SavedLoad?, animated: Bool = true) {
let target = SystemNavigationTarget(
system: system,
presentSystemEditor: presentSystemEditor,
loadToOpenOnAppear: loadToOpen
)
if animated {
systemNavigationTarget = target
} else {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
systemNavigationTarget = target
}
}
}
@discardableResult
private func makeSystem() -> ElectricalSystem {
let existingNames = Set(systems.map { $0.name }) let existingNames = Set(systems.map { $0.name })
var systemName = "New System" var systemName = "New System"
var counter = 1 var counter = 1
@@ -188,14 +247,47 @@ struct SystemsView: View {
colorName: "blue" colorName: "blue"
) )
modelContext.insert(newSystem) modelContext.insert(newSystem)
return newSystem
newSystemToEdit = newSystem }
private func startComponentOnboarding() {
let system = makeSystem()
let load = createNewLoad(in: system)
navigateToSystem(system, presentSystemEditor: false, loadToOpen: load, animated: false)
}
private func createNewLoad(in system: ElectricalSystem) -> SavedLoad {
let newLoad = SavedLoad(
name: "New Load",
voltage: 12.0,
current: 5.0,
power: 60.0,
length: 10.0,
crossSection: 1.0,
iconName: "lightbulb",
colorName: "blue",
isWattMode: false,
system: system
)
modelContext.insert(newLoad)
return newLoad
} }
private func deleteSystems(offsets: IndexSet) { private func deleteSystems(offsets: IndexSet) {
withAnimation { withAnimation {
for index in offsets { for index in offsets {
modelContext.delete(systems[index]) let system = systems[index]
deleteLoads(for: system)
modelContext.delete(system)
}
}
}
private func deleteLoads(for system: ElectricalSystem) {
let descriptor = FetchDescriptor<SavedLoad>()
if let loads = try? modelContext.fetch(descriptor) {
for load in loads where load.system == system {
modelContext.delete(load)
} }
} }
} }
@@ -225,11 +317,18 @@ struct LoadsView: View {
@EnvironmentObject var unitSettings: UnitSystemSettings @EnvironmentObject var unitSettings: UnitSystemSettings
@Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad] @Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad]
@State private var newLoadToEdit: SavedLoad? @State private var newLoadToEdit: SavedLoad?
@State private var showingSystemEditor = false
@State private var hasPresentedSystemEditorOnAppear = false
@State private var hasOpenedLoadOnAppear = false
let system: ElectricalSystem let system: ElectricalSystem
private let presentSystemEditorOnAppear: Bool
private let loadToOpenOnAppear: SavedLoad?
init(system: ElectricalSystem) { init(system: ElectricalSystem, presentSystemEditorOnAppear: Bool = false, loadToOpenOnAppear: SavedLoad? = nil) {
self.system = system self.system = system
self.presentSystemEditorOnAppear = presentSystemEditorOnAppear
self.loadToOpenOnAppear = loadToOpenOnAppear
} }
private var savedLoads: [SavedLoad] { private var savedLoads: [SavedLoad] {
@@ -237,117 +336,173 @@ struct LoadsView: View {
} }
var body: some View { var body: some View {
NavigationStack { VStack(spacing: 0) {
VStack(spacing: 0) { librarySection
librarySection
if savedLoads.isEmpty {
if savedLoads.isEmpty { emptyStateView
emptyStateView } else {
} else { List {
List { ForEach(savedLoads) { load in
ForEach(savedLoads) { load in NavigationLink(destination: CalculatorView(savedLoad: load)) {
NavigationLink(destination: CalculatorView(savedLoad: load)) { HStack(spacing: 12) {
HStack(spacing: 12) { ZStack {
ZStack { RoundedRectangle(cornerRadius: 10)
RoundedRectangle(cornerRadius: 10) .fill(colorForName(load.colorName))
.fill(colorForName(load.colorName)) .frame(width: 44, height: 44)
.frame(width: 44, height: 44)
Image(systemName: load.iconName)
Image(systemName: load.iconName) .font(.title3)
.font(.title3) .foregroundColor(.white)
.foregroundColor(.white) }
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(load.name)
.fontWeight(.medium)
Spacer()
Text(load.timestamp, format: .dateTime.month().day().hour().minute())
.foregroundColor(.secondary)
.font(.caption)
} }
VStack(alignment: .leading, spacing: 6) {
HStack { // Secondary info
Text(load.name) HStack {
Group {
Text(String(format: "%.1fV", load.voltage))
Text("")
if load.isWattMode {
Text(String(format: "%.0fW", load.power))
} else {
Text(String(format: "%.1fA", load.current))
}
Text("")
Text(String(format: "%.1f%@",
unitSettings.unitSystem == .metric ? load.length : load.length * 3.28084,
unitSettings.unitSystem.lengthUnit))
}
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
// Prominent fuse and wire gauge display
HStack(spacing: 12) {
HStack(spacing: 4) {
Text("FUSE")
.font(.caption2)
.fontWeight(.medium) .fontWeight(.medium)
Spacer()
Text(load.timestamp, format: .dateTime.month().day().hour().minute())
.foregroundColor(.secondary) .foregroundColor(.secondary)
.font(.caption) Text("\(recommendedFuse(for: load))A")
.font(.subheadline)
.fontWeight(.bold)
.foregroundColor(.orange)
} }
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.orange.opacity(0.1))
.cornerRadius(6)
HStack(spacing: 4) {
// Secondary info Text("WIRE")
HStack { .font(.caption2)
Group { .fontWeight(.medium)
Text(String(format: "%.1fV", load.voltage)) .foregroundColor(.secondary)
Text("") Text(String(format: unitSettings.unitSystem == .imperial ? "%.0f AWG" : "%.1fmm²",
if load.isWattMode { unitSettings.unitSystem == .imperial ? awgFromCrossSection(load.crossSection) : load.crossSection))
Text(String(format: "%.0fW", load.power)) .font(.subheadline)
} else { .fontWeight(.bold)
Text(String(format: "%.1fA", load.current)) .foregroundColor(.blue)
}
Text("")
Text(String(format: "%.1f%@",
unitSettings.unitSystem == .metric ? load.length : load.length * 3.28084,
unitSettings.unitSystem.lengthUnit))
}
.font(.caption)
.foregroundColor(.secondary)
Spacer()
} }
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.1))
.cornerRadius(6)
// Prominent fuse and wire gauge display Spacer()
HStack(spacing: 12) {
HStack(spacing: 4) {
Text("FUSE")
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(.secondary)
Text("\(recommendedFuse(for: load))A")
.font(.subheadline)
.fontWeight(.bold)
.foregroundColor(.orange)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.orange.opacity(0.1))
.cornerRadius(6)
HStack(spacing: 4) {
Text("WIRE")
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(.secondary)
Text(String(format: unitSettings.unitSystem == .imperial ? "%.0f AWG" : "%.1fmm²",
unitSettings.unitSystem == .imperial ? awgFromCrossSection(load.crossSection) : load.crossSection))
.font(.subheadline)
.fontWeight(.bold)
.foregroundColor(.blue)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.1))
.cornerRadius(6)
Spacer()
}
} }
} }
.padding(.vertical, 4)
} }
.padding(.vertical, 4)
} }
.onDelete(perform: deleteLoads) }
.onDelete(perform: deleteLoads)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Button(action: {
showingSystemEditor = true
}) {
HStack(spacing: 8) {
ZStack {
RoundedRectangle(cornerRadius: 6)
.fill(colorForName(system.colorName))
.frame(width: 24, height: 24)
Image(systemName: system.iconName)
.font(.system(size: 12))
.foregroundColor(.white)
}
Text(system.name)
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
} }
} }
} }
.navigationTitle(system.name)
.toolbar { ToolbarItem(placement: .navigationBarTrailing) {
ToolbarItem(placement: .navigationBarTrailing) { HStack {
HStack { Button(action: {
Button(action: { createNewLoad()
createNewLoad() }) {
}) { Image(systemName: "plus")
Image(systemName: "plus")
}
EditButton()
} }
EditButton()
} }
} }
.navigationDestination(item: $newLoadToEdit) { load in }
CalculatorView(savedLoad: load) .navigationDestination(item: $newLoadToEdit) { load in
CalculatorView(savedLoad: load)
}
.sheet(isPresented: $showingSystemEditor) {
SystemEditorView(
systemName: Binding(
get: { system.name },
set: { system.name = $0 }
),
location: Binding(
get: { system.location },
set: { system.location = $0 }
),
iconName: Binding(
get: { system.iconName },
set: { system.iconName = $0 }
),
colorName: Binding(
get: { system.colorName },
set: { system.colorName = $0 }
)
)
}
.onAppear {
if presentSystemEditorOnAppear && !hasPresentedSystemEditorOnAppear {
hasPresentedSystemEditorOnAppear = true
DispatchQueue.main.async {
showingSystemEditor = true
}
}
if let loadToOpen = loadToOpenOnAppear, !hasOpenedLoadOnAppear {
hasOpenedLoadOnAppear = true
DispatchQueue.main.async {
newLoadToEdit = loadToOpen
}
} }
} }
} }
@@ -393,94 +548,50 @@ struct LoadsView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
Spacer() Spacer()
VStack(spacing: 24) { VStack(spacing: 20) {
// Icon
ZStack { ZStack {
Circle() Circle()
.fill(Color.blue.opacity(0.1)) .fill(Color.blue.opacity(0.1))
.frame(width: 80, height: 80) .frame(width: 72, height: 72)
Image(systemName: "bolt.circle") Image(systemName: "bolt.circle")
.font(.system(size: 40)) .font(.system(size: 34))
.foregroundColor(.blue) .foregroundColor(.blue)
} }
// Title and subtitle VStack(spacing: 6) {
VStack(spacing: 8) { Text("No Components Yet")
Text("No Loads Yet") .font(.title3)
.font(.title2)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Get started by creating a new electrical load calculation or browse components from the VoltPlan library.") Text("Add a component to this system to see cable and fuse recommendations.")
.font(.body) .font(.body)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, 32) .padding(.horizontal, 32)
} }
// Action buttons Button(action: {
VStack(spacing: 12) { createNewLoad()
Button(action: { }) {
createNewLoad() HStack(spacing: 8) {
}) { Image(systemName: "plus.circle.fill")
HStack(spacing: 8) { .font(.system(size: 16))
Image(systemName: "plus.circle.fill") Text("Create Component")
.font(.system(size: 16)) .fontWeight(.medium)
Text("Create New Load")
.fontWeight(.medium)
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue)
.cornerRadius(12)
} }
.buttonStyle(.plain) .foregroundColor(.white)
.frame(maxWidth: .infinity)
Button(action: { .frame(height: 48)
// TODO: Open VoltPlan component library .background(Color.blue)
print("Opening VoltPlan component library...") .cornerRadius(12)
}) {
HStack(spacing: 8) {
Image(systemName: "square.grid.3x3")
.font(.system(size: 16))
Text("Browse VoltPlan Library")
.fontWeight(.medium)
Image(systemName: "arrow.up.right")
.font(.system(size: 12))
}
.foregroundColor(.blue)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue.opacity(0.1))
.cornerRadius(12)
}
.buttonStyle(.plain)
} }
.buttonStyle(.plain)
.padding(.horizontal, 32) .padding(.horizontal, 32)
} }
Spacer() Spacer()
// Safety disclaimer
VStack(spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.font(.system(size: 16))
Text("Important Safety Notice")
.font(.headline)
.fontWeight(.semibold)
}
Text("This app provides estimates for educational purposes only. Always consult qualified electricians and follow local electrical codes for actual installations. Electrical work can be dangerous and should only be performed by licensed professionals.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
}
.padding(.bottom, 32)
} }
} }

View File

@@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import UIKit
struct ItemEditorView: View { struct ItemEditorView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@@ -93,8 +94,11 @@ struct ItemEditorView: View {
} }
Section("Details") { Section("Details") {
TextField(nameFieldLabel, text: $tempName) AutoSelectTextField(
.autocapitalization(.words) text: $tempName,
placeholder: nameFieldLabel,
autoFocus: true
)
additionalFields() additionalFields()
} }
@@ -174,4 +178,58 @@ struct ItemEditorView: View {
iconName = tempIconName iconName = tempIconName
colorName = tempColorName colorName = tempColorName
} }
} }
private struct AutoSelectTextField: UIViewRepresentable {
@Binding var text: String
let placeholder: String
let autoFocus: Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = placeholder
textField.text = text
textField.autocapitalizationType = .words
textField.clearButtonMode = .whileEditing
textField.returnKeyType = .done
textField.addTarget(context.coordinator, action: #selector(Coordinator.textChanged(_:)), for: .editingChanged)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
uiView.placeholder = placeholder
uiView.autocapitalizationType = .words
if autoFocus, !context.coordinator.didFocus {
context.coordinator.didFocus = true
DispatchQueue.main.async {
uiView.becomeFirstResponder()
uiView.selectAll(nil)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
final class Coordinator: NSObject, UITextFieldDelegate {
@Binding var text: String
var didFocus = false
init(text: Binding<String>) {
self._text = text
}
@objc func textChanged(_ sender: UITextField) {
text = sender.text ?? ""
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
}

View File

@@ -16,10 +16,10 @@ struct SystemEditorView: View {
@State private var tempLocation: String @State private var tempLocation: String
private let systemIcons = [ private let systemIcons = [
"building.2", "house", "building", "factory", "office.building", "tent", "car.garage", "sailboat", "building.2", "house", "building", "tent", "sailboat",
"airplane", "ferry", "bus", "truck.box", "van.side", "rv", "airplane", "ferry", "bus", "truck.box",
"server.rack", "externaldrive", "cpu", "gear", "wrench.adjustable", "hammer", "server.rack", "externaldrive", "cpu", "gear", "wrench.adjustable", "hammer",
"lightbulb", "bolt", "powerplug", "battery.100", "solar.panel", "windmill", "lightbulb", "bolt", "powerplug", "battery.100","sun.max",
"engine.combustion", "fuelpump", "drop", "flame", "snowflake", "thermometer" "engine.combustion", "fuelpump", "drop", "flame", "snowflake", "thermometer"
] ]
@@ -63,4 +63,4 @@ struct SystemEditorView: View {
@State var color = "blue" @State var color = "blue"
return SystemEditorView(systemName: $name, location: $location, iconName: $icon, colorName: $color) return SystemEditorView(systemName: $name, location: $location, iconName: $icon, colorName: $color)
} }

View File

@@ -10,8 +10,41 @@ import Testing
struct CableTests { struct CableTests {
@Test func example() async throws { @Test func metricWireSizingUsesNearestStandardSize() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions. let calculator = CableCalculator()
calculator.voltage = 12
calculator.current = 5
calculator.length = 10 // meters
let crossSection = calculator.recommendedCrossSection(for: .metric)
#expect(crossSection == 4.0)
let voltageDrop = calculator.voltageDrop(for: .metric)
#expect(abs(voltageDrop - 0.425) < 0.001)
let dropPercentage = calculator.voltageDropPercentage(for: .metric)
#expect(abs(dropPercentage - 3.5417) < 0.001)
let powerLoss = calculator.powerLoss(for: .metric)
#expect(abs(powerLoss - 2.125) < 0.001)
} }
@Test func imperialWireSizingMatchesExpectedGauge() async throws {
let calculator = CableCalculator()
calculator.voltage = 120
calculator.current = 15
calculator.length = 25 // feet
let awg = calculator.recommendedCrossSection(for: .imperial)
#expect(awg == 18.0)
let voltageDrop = calculator.voltageDrop(for: .imperial)
#expect(abs(voltageDrop - 4.722) < 0.01)
let dropPercentage = calculator.voltageDropPercentage(for: .imperial)
#expect(abs(dropPercentage - 3.935) < 0.01)
let powerLoss = calculator.powerLoss(for: .imperial)
#expect(abs(powerLoss - 70.83) < 0.05)
}
} }