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

View File

@@ -30,8 +30,22 @@ struct SystemsView: View {
@Environment(\.modelContext) private var modelContext
@EnvironmentObject var unitSettings: UnitSystemSettings
@Query(sort: \ElectricalSystem.timestamp, order: .reverse) private var systems: [ElectricalSystem]
@State private var newSystemToEdit: ElectricalSystem?
@State private var systemToEdit: ElectricalSystem?
@State private var systemNavigationTarget: SystemNavigationTarget?
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 {
NavigationStack {
@@ -52,9 +66,6 @@ struct SystemsView: View {
.font(.title3)
.foregroundColor(.white)
}
.onTapGesture {
systemToEdit = system
}
VStack(alignment: .leading, spacing: 4) {
Text(system.name)
@@ -93,27 +104,11 @@ struct SystemsView: View {
}
}
}
.navigationDestination(item: $newSystemToEdit) { system in
LoadsView(system: system)
}
.sheet(item: $systemToEdit) { system in
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 }
)
.navigationDestination(item: $systemNavigationTarget) { target in
LoadsView(
system: target.system,
presentSystemEditorOnAppear: target.presentSystemEditor,
loadToOpenOnAppear: target.loadToOpenOnAppear
)
}
}
@@ -129,49 +124,113 @@ struct SystemsView: View {
.fill(Color.blue.opacity(0.1))
.frame(width: 80, height: 80)
Image(systemName: "building.2")
Image(systemName: "bolt.circle")
.font(.system(size: 40))
.foregroundColor(.blue)
}
VStack(spacing: 8) {
Text("No Systems Yet")
Text("Welcome to Cable by VoltPlan")
.font(.title2)
.fontWeight(.semibold)
.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)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, 32)
}
Button(action: {
createNewSystem()
}) {
HStack(spacing: 8) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 16))
Text("Create System")
.fontWeight(.medium)
VStack(spacing: 12) {
Button(action: {
startComponentOnboarding()
}) {
HStack(spacing: 8) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 16))
Text("Create Component")
.fontWeight(.medium)
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue)
.cornerRadius(12)
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue)
.cornerRadius(12)
.buttonStyle(.plain)
Button(action: {
// TODO: Open VoltPlan component library
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)
}
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() {
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 })
var systemName = "New System"
var counter = 1
@@ -188,14 +247,47 @@ struct SystemsView: View {
colorName: "blue"
)
modelContext.insert(newSystem)
newSystemToEdit = newSystem
return 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) {
withAnimation {
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
@Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad]
@State private var newLoadToEdit: SavedLoad?
@State private var showingSystemEditor = false
@State private var hasPresentedSystemEditorOnAppear = false
@State private var hasOpenedLoadOnAppear = false
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.presentSystemEditorOnAppear = presentSystemEditorOnAppear
self.loadToOpenOnAppear = loadToOpenOnAppear
}
private var savedLoads: [SavedLoad] {
@@ -237,117 +336,173 @@ struct LoadsView: View {
}
var body: some View {
NavigationStack {
VStack(spacing: 0) {
librarySection
if savedLoads.isEmpty {
emptyStateView
} else {
List {
ForEach(savedLoads) { load in
NavigationLink(destination: CalculatorView(savedLoad: load)) {
HStack(spacing: 12) {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(colorForName(load.colorName))
.frame(width: 44, height: 44)
Image(systemName: load.iconName)
.font(.title3)
.foregroundColor(.white)
VStack(spacing: 0) {
librarySection
if savedLoads.isEmpty {
emptyStateView
} else {
List {
ForEach(savedLoads) { load in
NavigationLink(destination: CalculatorView(savedLoad: load)) {
HStack(spacing: 12) {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(colorForName(load.colorName))
.frame(width: 44, height: 44)
Image(systemName: load.iconName)
.font(.title3)
.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 {
Text(load.name)
// Secondary info
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)
Spacer()
Text(load.timestamp, format: .dateTime.month().day().hour().minute())
.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)
// Secondary info
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()
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)
// Prominent fuse and wire gauge display
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()
}
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) {
HStack {
Button(action: {
createNewLoad()
}) {
Image(systemName: "plus")
}
EditButton()
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
Button(action: {
createNewLoad()
}) {
Image(systemName: "plus")
}
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) {
Spacer()
VStack(spacing: 24) {
// Icon
VStack(spacing: 20) {
ZStack {
Circle()
.fill(Color.blue.opacity(0.1))
.frame(width: 80, height: 80)
.frame(width: 72, height: 72)
Image(systemName: "bolt.circle")
.font(.system(size: 40))
.font(.system(size: 34))
.foregroundColor(.blue)
}
// Title and subtitle
VStack(spacing: 8) {
Text("No Loads Yet")
.font(.title2)
VStack(spacing: 6) {
Text("No Components Yet")
.font(.title3)
.fontWeight(.semibold)
.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)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
}
// Action buttons
VStack(spacing: 12) {
Button(action: {
createNewLoad()
}) {
HStack(spacing: 8) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 16))
Text("Create New Load")
.fontWeight(.medium)
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.blue)
.cornerRadius(12)
Button(action: {
createNewLoad()
}) {
HStack(spacing: 8) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 16))
Text("Create Component")
.fontWeight(.medium)
}
.buttonStyle(.plain)
Button(action: {
// TODO: Open VoltPlan component library
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)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 48)
.background(Color.blue)
.cornerRadius(12)
}
.buttonStyle(.plain)
.padding(.horizontal, 32)
}
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 UIKit
struct ItemEditorView: View {
@Environment(\.dismiss) private var dismiss
@@ -93,8 +94,11 @@ struct ItemEditorView: View {
}
Section("Details") {
TextField(nameFieldLabel, text: $tempName)
.autocapitalization(.words)
AutoSelectTextField(
text: $tempName,
placeholder: nameFieldLabel,
autoFocus: true
)
additionalFields()
}
@@ -174,4 +178,58 @@ struct ItemEditorView: View {
iconName = tempIconName
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
private let systemIcons = [
"building.2", "house", "building", "factory", "office.building", "tent", "car.garage", "sailboat",
"airplane", "ferry", "bus", "truck.box", "van.side", "rv",
"building.2", "house", "building", "tent", "sailboat",
"airplane", "ferry", "bus", "truck.box",
"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"
]
@@ -63,4 +63,4 @@ struct SystemEditorView: View {
@State var color = "blue"
return SystemEditorView(systemName: $name, location: $location, iconName: $icon, colorName: $color)
}
}