import SwiftUI /// Der Einstieg: das Wichtigste untereinander, in der Reihenfolge, in der man /// im Wohnmobil danach schaut. struct WatchRootView: View { @Environment(PhoneLink.self) private var link @Environment(WatchLevelRadio.self) private var radio /// Der Weg durch die Ansichten liegt hier, damit ihn auch die Komplikation /// setzen kann: Ein Tipp aufs Zifferblatt springt direkt zur Nivellierung, /// statt in der Geräteliste zu landen. @State private var path: [WatchRoute] = [] var body: some View { NavigationStack(path: $path) { content .navigationTitle(link.payload?.profile.name ?? "Camper") .navigationDestination(for: WatchRoute.self) { route in switch route { case .level: WatchLevelView() case .align: WatchAlignView() case .device(let id): WatchDeviceDetailView(deviceID: id) } } } .onOpenURL { url in guard WatchDeepLink.isLevel(url) else { return } // Nicht anhängen, sondern ersetzen: Wer vom Zifferblatt kommt, will // die Nivellierung sehen und nicht einen Stapel von vorhin. path = [.level] } } private var content: some View { List { // Die Nivellierung steht immer oben und immer da: Sie kommt vom // Sensor selbst und braucht das iPhone nicht. Section { NavigationLink(value: WatchRoute.level) { LevelSummaryRow(reading: WatchLevelReading(radio: radio, phone: link)) } } if let payload = link.payload { Section { ForEach(payload.devices.filter { $0.role != .leveling }) { device in NavigationLink(value: WatchRoute.device(device.id)) { DeviceRow(device: device) } } } Section { FreshnessNote(payload: payload, receivedAt: link.receivedAt) } } else { Section { waiting } footer: { Text("Batterie, Solar und Kühlbox kommen vom iPhone. Die " + "Nivellierung oben nicht – die geht direkt.") .font(.system(size: 10)) } } } } private var waiting: some View { VStack(spacing: 4) { Label("Warte auf das iPhone", systemImage: "iphone.gen3.radiowaves.left.and.right") .font(.footnote.weight(.medium)) Text("Dort muss VanControl Pro geöffnet sein.") .font(.caption2) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) } } /// Wohin die Uhr springen kann. Als eigener Typ, weil der Weg von aussen /// gesetzt wird – von der Komplikation. enum WatchRoute: Hashable { case level case align case device(UUID) } /// Eine Gerätezeile: Name klein, Hauptwert gross – auf dem kleinen Bildschirm /// zählt nur der eine Wert, wegen dem man den Arm hebt. struct DeviceRow: View { let device: WatchDevice var body: some View { HStack(spacing: 8) { Image(systemName: device.role.symbol) .foregroundStyle(.tint) .frame(width: 20) VStack(alignment: .leading, spacing: 1) { Text(device.name) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) if let metric = device.snapshot?.primaryMetric, metric.value != nil { Text(metric.formattedWithUnit) .font(.title3.weight(.semibold)) .monospacedDigit() .foregroundStyle(device.isLive ? .primary : .secondary) } else { Text(device.link.label) .font(.footnote) .foregroundStyle(.secondary) } } Spacer(minLength: 0) Circle() .fill(device.statusColor) .frame(width: 7, height: 7) } } } /// Die Nivellierung bekommt eine eigene, grössere Zeile: sie ist der Grund, /// aus dem man die Uhr im Fahrzeug überhaupt anschaut. struct LevelSummaryRow: View { let reading: WatchLevelReading var body: some View { HStack(spacing: 10) { WatchBubble(pitch: reading.state.pitch, roll: reading.state.roll) .frame(width: 44, height: 44) VStack(alignment: .leading, spacing: 2) { Text("Nivellierung") .font(.caption2) .foregroundStyle(.secondary) // Ohne Messwerte steht hier, woran es liegt, statt eines // nichtssagenden Strichs. Text(reading.hasReading ? (reading.state.instruction ?? "Keine Werte") : reading.statusText) .font(.footnote.weight(.medium)) .lineLimit(2) } Spacer(minLength: 0) } .padding(.vertical, 2) } } /// Wie alt der Stand ist – auf der Uhr die wichtigste Nebenangabe. Alles hier /// ist weitergereicht; eine abgerissene Strecke zum iPhone sieht sonst genauso /// aus wie ein Fahrzeug, an dem sich nichts tut. struct FreshnessNote: View { let payload: WatchPayload let receivedAt: Date? var body: some View { TimelineView(.periodic(from: .now, by: 1)) { context in let age = context.date.timeIntervalSince(receivedAt ?? payload.generatedAt) Label { Text(text(age: age)) .font(.caption2) } icon: { Image(systemName: age > 10 ? "exclamationmark.triangle.fill" : "iphone.gen3") } .foregroundStyle(age > 10 ? .orange : .secondary) } } private func text(age: TimeInterval) -> String { if !payload.isRadioReady { return payload.radioStatus } if age < 3 { return "Stand: eben" } if age < 90 { return String(format: "Stand: vor %.0f s", age) } return String(format: "Stand: vor %.0f min – iPhone erreichbar?", age / 60) } } extension WatchDevice { /// Grün heisst: der Wert ist frisch und kommt vom Gerät selbst. var statusColor: Color { switch link { case .live: return (snapshot?.isStale ?? true) ? .orange : .green case .needsKey: return .orange case .failed: return .red default: return .gray } } }