forked from fritob/Camper-Monitor
Eigene Build-Konfiguration "CarPlay" samt Schema, damit der Versuchsbau neben der TestFlight-App stehen kann: eigener Name, eigene Bundle-ID, persoenliches Team, manuelles Signing. Debug und Release bleiben unberuehrt. Wichtig dabei: Die Signing-Einstellungen stehen ausschliesslich in den Target-Konfigurationen. Steht CODE_SIGN_STYLE auf Projektebene, schreibt der naechste xcodebuild-Lauf alle uebrigen Konfigurationen auf Manual um und loescht ihr Team – der TestFlight-Build waere damit hin. Der CarPlay-Code haengt an `CARPLAY_DEMO` und existiert im regulaeren Build nicht. Die Szene zeichnet in das gelieferte CPWindow; das CPMapTemplate darueber bleibt leer, weil es auf dem Stellplatz nichts zu tippen gibt. Die Anzeige sperrt sich oberhalb von 5 km/h selbst – erzwungen, nicht zugesagt. Die Groessen sind auf die 400x240 Punkte ausgelegt, die CarPlay liefert, nicht auf die 800x480 Pixel. In Pixeln gedachte Schriftgroessen sind auf dem Fahrzeugdisplay doppelt zu gross und schneiden den Text ab. Bekannt: Unter iOS 26.5 stuerzt Apples CarPlayTemplateUIHost beim Aufbau der Navigationsleiste ab (`_updateShareButtonVisibility` fragt seinen destinationSharingDelegate nach `vehicleSupportsDestinationSharing`, den dieser nicht kennt). Beides ist privates API und von der App aus nicht erreichbar; unter iOS 18 laeuft es. Szene und Root-Template werden vorher in beiden Faellen akzeptiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
4.2 KiB
Swift
97 lines
4.2 KiB
Swift
#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
|