better battery editor view

This commit is contained in:
Stefan Lange-Hegermann
2025-10-21 23:00:56 +02:00
parent 9f8d8e5149
commit d081a79b59
5 changed files with 188 additions and 60 deletions

View File

@@ -255,7 +255,7 @@ struct BatteriesView: View {
private func batteryRow(for battery: SavedBattery) -> some View {
VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 12) {
batteryIcon
batteryIcon(for: battery)
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline) {
@@ -312,12 +312,12 @@ struct BatteriesView: View {
)
}
private var batteryIcon: some View {
private func batteryIcon(for battery: SavedBattery) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(colorForName(system.colorName))
.fill(colorForName(battery.colorName))
.frame(width: 48, height: 48)
Image(systemName: "battery.100.bolt")
Image(systemName: battery.iconName.isEmpty ? "battery.100.bolt" : battery.iconName)
.font(.system(size: 22, weight: .semibold))
.foregroundStyle(Color.white)
}
@@ -589,6 +589,8 @@ private enum BatteriesViewPreviewData {
nominalVoltage: 12.8,
capacityAmpHours: 200,
chemistry: .lithiumIronPhosphate,
iconName: "battery.100.bolt",
colorName: "green",
system: system
),
SavedBattery(
@@ -596,6 +598,8 @@ private enum BatteriesViewPreviewData {
nominalVoltage: 12.0,
capacityAmpHours: 90,
chemistry: .agm,
iconName: "bolt",
colorName: "orange",
system: system
)
]

View File

@@ -21,6 +21,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
var nominalVoltage: Double
var capacityAmpHours: Double
var chemistry: Chemistry
var iconName: String
var colorName: String
var system: ElectricalSystem
init(
@@ -29,6 +31,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
nominalVoltage: Double = 12.8,
capacityAmpHours: Double = 100,
chemistry: Chemistry = .lithiumIronPhosphate,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem
) {
self.id = id
@@ -36,6 +40,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.nominalVoltage = nominalVoltage
self.capacityAmpHours = capacityAmpHours
self.chemistry = chemistry
self.iconName = iconName
self.colorName = colorName
self.system = system
}
@@ -45,6 +51,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.nominalVoltage = savedBattery.nominalVoltage
self.capacityAmpHours = savedBattery.capacityAmpHours
self.chemistry = savedBattery.chemistry
self.iconName = savedBattery.iconName
self.colorName = savedBattery.colorName
self.system = system
}
@@ -57,6 +65,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
savedBattery.nominalVoltage = nominalVoltage
savedBattery.capacityAmpHours = capacityAmpHours
savedBattery.chemistry = chemistry
savedBattery.iconName = iconName
savedBattery.colorName = colorName
savedBattery.system = system
savedBattery.timestamp = Date()
}
@@ -68,7 +78,9 @@ extension BatteryConfiguration {
lhs.name == rhs.name &&
lhs.nominalVoltage == rhs.nominalVoltage &&
lhs.capacityAmpHours == rhs.capacityAmpHours &&
lhs.chemistry == rhs.chemistry
lhs.chemistry == rhs.chemistry &&
lhs.iconName == rhs.iconName &&
lhs.colorName == rhs.colorName
}
func hash(into hasher: inout Hasher) {
@@ -77,5 +89,7 @@ extension BatteryConfiguration {
hasher.combine(nominalVoltage)
hasher.combine(capacityAmpHours)
hasher.combine(chemistry)
hasher.combine(iconName)
hasher.combine(colorName)
}
}

View File

@@ -5,6 +5,7 @@ struct BatteryEditorView: View {
@State private var editingField: EditingField?
@State private var voltageInput: String = ""
@State private var capacityInput: String = ""
@State private var showingAppearanceEditor = false
let onSave: (BatteryConfiguration) -> Void
private enum EditingField {
@@ -16,6 +17,21 @@ struct BatteryEditorView: View {
private let capacitySnapValues: [Double] = [10, 20, 50, 75, 100, 125, 150, 200, 300, 400, 600, 800, 1000]
private let voltageSnapTolerance: Double = 0.5
private let capacitySnapTolerance: Double = 10.0
private let batteryIconOptions: [String] = [
"battery.100",
"battery.100.bolt",
"battery.75",
"battery.25",
"battery.0",
"bolt",
"bolt.fill",
"bolt.circle",
"bolt.horizontal.circle",
"powerplug",
"car.battery",
"bolt.square",
"lightbulb"
]
private var nameFieldLabel: String {
String(
@@ -88,6 +104,41 @@ struct BatteryEditorView: View {
comment: "Label used for energy values"
)
}
private var appearanceEditorTitle: String {
NSLocalizedString(
"battery.editor.appearance.title",
bundle: .main,
value: "Battery Appearance",
comment: "Title for the battery appearance editor"
)
}
private var appearanceEditorSubtitle: String {
NSLocalizedString(
"battery.editor.appearance.subtitle",
bundle: .main,
value: "Customize how this battery shows up",
comment: "Subtitle shown in the battery appearance editor preview"
)
}
private var appearanceAccessibilityLabel: String {
NSLocalizedString(
"battery.editor.appearance.accessibility",
bundle: .main,
value: "Edit battery appearance",
comment: "Accessibility label for the battery appearance editor button"
)
}
private var iconColor: Color {
colorForName(configuration.colorName)
}
private var displayName: String {
configuration.name.isEmpty ? namePlaceholder : configuration.name
}
private var voltageSliderRange: ClosedRange<Double> {
let lowerBound = max(0, min(6, configuration.nominalVoltage))
@@ -107,27 +158,44 @@ struct BatteryEditorView: View {
}
var body: some View {
ScrollView {
VStack(spacing: 24) {
headerCard
slidersSection
}
.padding(.vertical, 24)
.padding(.horizontal)
List {
configurationSection
sliderSection
}
.listStyle(.plain)
.scrollIndicators(.hidden)
.scrollContentBackground(.hidden)
.background(Color(.systemGroupedBackground).ignoresSafeArea())
.navigationTitle(
NSLocalizedString(
"battery.editor.title",
bundle: .main,
value: "Battery Setup",
comment: "Title for the battery editor"
)
)
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
navigationTitleView
}
}
.onDisappear {
onSave(configuration)
}
.sheet(isPresented: $showingAppearanceEditor) {
ItemEditorView(
title: appearanceEditorTitle,
nameFieldLabel: nameFieldLabel,
previewSubtitle: appearanceEditorSubtitle,
icons: batteryIconOptions,
name: Binding(
get: { configuration.name },
set: { configuration.name = $0 }
),
iconName: Binding(
get: { configuration.iconName },
set: { configuration.iconName = $0 }
),
colorName: Binding(
get: { configuration.colorName },
set: { configuration.colorName = $0 }
)
)
}
.alert(
NSLocalizedString(
"battery.editor.alert.voltage.title",
@@ -278,26 +346,32 @@ struct BatteryEditorView: View {
}
}
private var headerCard: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text(nameFieldLabel)
.font(.caption)
.foregroundStyle(.secondary)
TextField(namePlaceholder, text: $configuration.name)
.textInputAutocapitalization(.words)
.padding(.vertical, 10)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(.secondarySystemBackground))
)
private var navigationTitleView: some View {
Button {
showingAppearanceEditor = true
} label: {
HStack(spacing: 8) {
LoadIconView(
remoteIconURLString: nil,
fallbackSystemName: configuration.iconName.isEmpty ? "battery.100.bolt" : configuration.iconName,
fallbackColor: iconColor,
size: 26
)
Text(displayName)
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(.primary)
}
}
.buttonStyle(.plain)
.accessibilityLabel(appearanceAccessibilityLabel)
}
private var configurationSection: some View {
Section {
VStack(alignment: .leading, spacing: 8) {
Text(chemistryLabel)
.font(.caption)
.foregroundStyle(.secondary)
Text(chemistryLabel.uppercased())
.font(.headline)
Menu {
ForEach(BatteryConfiguration.Chemistry.allCases) { chemistry in
Button {
@@ -313,25 +387,29 @@ struct BatteryEditorView: View {
} label: {
HStack {
Text(configuration.chemistry.displayName)
.font(.body.weight(.semibold))
.font(.title)
.fontWeight(.bold)
Spacer()
Image(systemName: "chevron.down")
.font(.footnote.weight(.bold))
.foregroundStyle(.secondary)
}
.padding(.vertical, 10)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(.secondarySystemBackground))
)
}
.buttonStyle(.plain)
}
.padding(.vertical, 6)
}
.listRowBackground(Color(.systemBackground))
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
VStack(alignment: .leading, spacing: 6) {
Text(summaryLabel)
.font(.caption)
private var summarySection: some View {
Section {
VStack(alignment: .leading, spacing: 8) {
Text(summaryLabel.uppercased())
.font(.caption2)
.fontWeight(.medium)
.foregroundStyle(.secondary)
ViewThatFits(in: .horizontal) {
HStack(spacing: 16) {
@@ -371,16 +449,15 @@ struct BatteryEditorView: View {
}
}
}
.padding(.vertical, 6)
}
.padding(20)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color(.tertiarySystemBackground))
)
.listRowBackground(Color(.systemBackground))
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
private var slidersSection: some View {
VStack(spacing: 30) {
private var sliderSection: some View {
Section {
SliderSection(
title: sliderVoltageTitle,
value: Binding(
@@ -398,6 +475,7 @@ struct BatteryEditorView: View {
tapAction: beginVoltageEditing,
snapValues: editingField == .voltage ? nil : voltageSnapValues
)
.listRowSeparator(.hidden)
SliderSection(
title: sliderCapacityTitle,
@@ -416,12 +494,10 @@ struct BatteryEditorView: View {
tapAction: beginCapacityEditing,
snapValues: editingField == .capacity ? nil : capacitySnapValues
)
.listRowSeparator(.hidden)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color(.secondarySystemBackground))
)
.listRowBackground(Color(.systemBackground))
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
private func normalizedVoltage(for value: Double) -> Double {
@@ -475,6 +551,25 @@ struct BatteryEditorView: View {
return abs(closest - value) <= tolerance ? closest : nil
}
private func colorForName(_ colorName: String) -> Color {
switch colorName {
case "blue": return .blue
case "green": return .green
case "orange": return .orange
case "red": return .red
case "purple": return .purple
case "yellow": return .yellow
case "pink": return .pink
case "teal": return .teal
case "indigo": return .indigo
case "mint": return .mint
case "cyan": return .cyan
case "brown": return .brown
case "gray": return .gray
default: return .blue
}
}
private func summaryBadge(title: String, value: String, symbol: String) -> some View {
VStack(spacing: 4) {
Image(systemName: symbol)
@@ -516,7 +611,12 @@ struct BatteryEditorView: View {
let previewSystem = ElectricalSystem(name: "Camper")
return NavigationStack {
BatteryEditorView(
configuration: BatteryConfiguration(name: "House Bank", system: previewSystem),
configuration: BatteryConfiguration(
name: "House Bank",
iconName: "battery.100.bolt",
colorName: "green",
system: previewSystem
),
onSave: { _ in }
)
}

View File

@@ -8,6 +8,8 @@ class SavedBattery {
var nominalVoltage: Double
var capacityAmpHours: Double
private var chemistryRawValue: String
var iconName: String = "battery.100"
var colorName: String = "blue"
var system: ElectricalSystem?
var timestamp: Date
@@ -17,6 +19,8 @@ class SavedBattery {
nominalVoltage: Double = 12.8,
capacityAmpHours: Double = 100,
chemistry: BatteryConfiguration.Chemistry = .lithiumIronPhosphate,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem? = nil,
timestamp: Date = Date()
) {
@@ -25,6 +29,8 @@ class SavedBattery {
self.nominalVoltage = nominalVoltage
self.capacityAmpHours = capacityAmpHours
self.chemistryRawValue = chemistry.rawValue
self.iconName = iconName
self.colorName = colorName
self.system = system
self.timestamp = timestamp
}

View File

@@ -100,6 +100,8 @@ struct SystemComponentsPersistence {
)
return BatteryConfiguration(
name: batteryName,
iconName: "battery.100.bolt",
colorName: system.colorName,
system: system
)
}
@@ -119,6 +121,8 @@ struct SystemComponentsPersistence {
nominalVoltage: configuration.nominalVoltage,
capacityAmpHours: configuration.capacityAmpHours,
chemistry: configuration.chemistry,
iconName: configuration.iconName,
colorName: configuration.colorName,
system: system
)
context.insert(newBattery)