Zwei Dinge in einem Commit, weil sie sich dieselben Dateien teilen: project.pbxproj und WatchRootView.swift tragen beides. **Komplikation.** Neues Target CamperMonitorComplication, auf watchOS sind Komplikationen WidgetKit-Widgets. Es wird in die Watch-App eingebettet (PlugIns) und bietet die vier Zubehör-Familien an. Ein Tipp öffnet die App direkt auf der Nivellierung statt in der Geräteliste: Die Komplikation hängt eine Adresse an, die Watch-App führt dafür jetzt einen Navigationsweg, den sie von aussen setzen kann. Die Adresse steht in Shared/WatchDeepLink.swift, weil beide Seiten sie brauchen – ein Tippfehler auf einer Seite bliebe sonst stumm. Bewusst ohne Messwert: Eine Komplikation läuft in einem eigenen Prozess und käme an die Werte nur über eine App-Gruppe, und die verlangt eine bezahlte Mitgliedschaft. Ein Wert, der beim Blick aufs Zifferblatt Stunden alt sein kann, wäre ohnehin schlimmer als keiner – man glaubt ihm. Die Komplikation tut deshalb genau eine Sache, und die zuverlässig. **Rebranding.** Der sichtbare Name ist überall VanControl Pro: CFBundleDisplayName beider Apple-Targets, Android-Label, die Texte in beiden Apps, beide READMEs und der Name der APK im Gitea-Ablauf. Die Kennungen bleiben, wie sie sind – Bundle-IDs, Android-Paket, Target-, Ordner- und Schemanamen. Eine geänderte Bundle-ID ist für iOS eine neue App: Geräte, Fahrzeugprofile und Victron-Schlüssel wären weg, dazu neue Profile und eine neu zu koppelnde Watch-App. Sichtbar ist davon nichts, also gibt es dafür auch keinen Grund. Geprüft: alle drei Apple-Targets bauen, die Erweiterung liegt im Watch-Bundle unter PlugIns, die Anzeigenamen stimmen im gebauten Bundle, Tests grün. Die neue Bundle-ID der Erweiterung braucht beim ersten Lauf aus Xcode ein Profil, das Xcode selbst anlegt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
49 lines
2.0 KiB
Swift
49 lines
2.0 KiB
Swift
import SwiftUI
|
||
|
||
/// VanControl Pro am Handgelenk.
|
||
///
|
||
/// Zwei Quellen, jede dort, wo sie hingehört:
|
||
///
|
||
/// * Der **Neigungsmesser** hängt direkt an der Uhr (`WatchLevelRadio`). Die
|
||
/// Nivellierung braucht damit kein geöffnetes iPhone – und genau dafür hebt
|
||
/// man beim Rangieren den Arm.
|
||
/// * **Batterie, Solar und Kühlbox** kommen über das iPhone (`PhoneLink`),
|
||
/// weil dort die Schlüssel liegen und die Geräte nur eine Verbindung
|
||
/// zulassen.
|
||
@main
|
||
struct CamperWatchApp: App {
|
||
@State private var link = PhoneLink()
|
||
@State private var radio = WatchLevelRadio()
|
||
@Environment(\.scenePhase) private var scenePhase
|
||
|
||
var body: some Scene {
|
||
WindowGroup {
|
||
WatchRootView()
|
||
.environment(link)
|
||
.environment(radio)
|
||
.task {
|
||
link.activate()
|
||
radio.start()
|
||
// Beim Start meldet `onChange` nichts: Die Bildschirmphase
|
||
// steht schon auf „aktiv“ und ändert sich nicht mehr. Ohne
|
||
// diese Zeile bliebe der schnelle Takt des iPhones aus, bis
|
||
// die App einmal im Hintergrund war.
|
||
link.setLive(true)
|
||
}
|
||
// Die Einbaulage kommt bevorzugt aus dem Sensor selbst. Nur
|
||
// falls dort noch nichts steht – ältere Firmware, oder nie
|
||
// bestimmt –, gilt, was das iPhone meldet.
|
||
.onChange(of: link.payload?.levelDevice?.orientation) { _, orientation in
|
||
guard let orientation, !radio.hasDeviceOrientation else { return }
|
||
radio.orientation = orientation
|
||
}
|
||
}
|
||
.onChange(of: scenePhase) { _, phase in
|
||
// Beides kostet Strom und lohnt nur, solange jemand hinschaut.
|
||
// Genau das sagt die Bildschirmphase aus.
|
||
link.setLive(phase == .active)
|
||
if phase == .active { radio.start() } else { radio.stop() }
|
||
}
|
||
}
|
||
}
|