#if CARPLAY_DEMO import CarPlay import OSLog import SwiftUI /// Einstiegspunkt der CarPlay-Szene. /// /// Mit dem Navigations-Entitlement liefert CarPlay ein eigenes `CPWindow`, in /// das frei gezeichnet werden darf – anders als bei den Template-Kategorien, /// die nur vorgegebene Bausteine erlauben und höchstens alle zehn Sekunden /// aktualisieren. Genau deshalb hängt hier die Libelle drin: Beim Rangieren /// muss sie der Bewegung des Fahrzeugs folgen, nicht ihr hinterherlaufen. /// /// Das `CPMapTemplate` darüber ist nur die Bedienleiste. Es bleibt bewusst /// leer: Auf dem Stellplatz gibt es nichts zu tippen, es gibt nur etwas /// abzulesen. @objc(CarPlaySceneDelegate) final class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate, CPMapTemplateDelegate { private var interfaceController: CPInterfaceController? /// Ohne eigenes Logging ist beim Debuggen nicht zu unterscheiden, ob die /// Szene gar nicht verbindet oder ob CarPlay das Template ablehnt. private static let log = Logger(subsystem: "de.s0.fototeddy.VanControl", category: "CarPlay") func templateApplicationScene( _ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController, to window: CPWindow ) { Self.log.notice("didConnect: Szene verbunden, Fenster \(NSCoder.string(for: window.bounds.size), privacy: .public)") self.interfaceController = interfaceController let root = CarPlayLevelScreen() .environment(CarPlayLevelSource.shared) let host = UIHostingController(rootView: root) host.view.backgroundColor = .black window.rootViewController = host window.makeKeyAndVisible() CarPlayLevelSource.shared.start() // Bewusst nackt konfiguriert: iOS 26 laesst den CarPlayTemplateUIHost // beim Aufbau der Navigationsleiste ueber den Share-Button stolpern // (`_updateShareButtonVisibility` -> unrecognized selector), sobald die // Leiste erzwungen wird. Ohne Buttons und mit der Standard-Automatik // gibt es nichts zu konfigurieren – und auf dem Stellplatz gibt es // ohnehin nichts zu tippen. let map = CPMapTemplate() map.mapButtons = [] map.leadingNavigationBarButtons = [] map.trailingNavigationBarButtons = [] map.hidesButtonsWithNavigationBar = true map.mapDelegate = self interfaceController.setRootTemplate(map, animated: false) { success, error in if let error { Self.log.error("setRootTemplate abgelehnt: \(error.localizedDescription, privacy: .public)") } else { Self.log.notice("setRootTemplate ok: \(success, privacy: .public)") } } } func templateApplicationScene( _ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController, from window: CPWindow ) { Self.log.notice("didDisconnect: Szene getrennt") self.interfaceController = nil window.rootViewController = nil CarPlayLevelSource.shared.stop() } // MARK: - Zielteilung // CarPlay bietet seit iOS 26 an, ein Navigationsziel an das Fahrzeug zu // übergeben. Wir haben keine Ziele – aber das Protokoll vollständig zu // bedienen ist Teil davon, eine korrekte Navigations-App zu sein. Der // Verdacht dabei: Apples Template-Host löst seinen Sharing-Delegate nur // dann sauber auf, wenn die App diese Methoden anbietet. @available(iOS 26.4, *) func mapTemplate(_ mapTemplate: CPMapTemplate, willShareDestinationFor trip: CPTrip) { Self.log.notice("willShareDestination – die App teilt keine Ziele") } @available(iOS 26.4, *) func mapTemplate(_ mapTemplate: CPMapTemplate, didShareDestinationFor trip: CPTrip) { Self.log.notice("didShareDestination") } @available(iOS 26.4, *) func mapTemplate(_ mapTemplate: CPMapTemplate, didFailToShareDestinationFor trip: CPTrip, error: Error) { Self.log.error("didFailToShareDestination: \(error.localizedDescription, privacy: .public)") } } #endif