forked from fritob/Camper-Monitor
Compare commits
2
Commits
8401f62d91
...
ae12d8ede4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae12d8ede4 | ||
|
|
b1ffbe324a |
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>bluetooth-central</string>
|
||||
</array>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Damit die Nivellierung auf dem CarPlay-Display nur im Stand angezeigt wird.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Damit die Nivellierung auf dem CarPlay-Display nur im Stand angezeigt wird.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>CarPlay</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>CarPlaySceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.carplay-maps</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -65,6 +65,25 @@ struct LevelState: Equatable, Codable, Sendable {
|
||||
return parts.joined(separator: ", ")
|
||||
}
|
||||
|
||||
/// Dasselbe wie ``instruction``, aber in kurzen Zeilen statt einem Satz –
|
||||
/// für schmale Anzeigen wie das CarPlay-Display, wo "Heck steht höher,
|
||||
/// rechts steht höher" nicht in eine lesbare Zeile passt.
|
||||
///
|
||||
/// Die Vorzeichen müssen zu ``instruction`` passen; beide stehen deshalb
|
||||
/// nebeneinander.
|
||||
var shortInstructions: [String] {
|
||||
guard let pitch, let roll else { return [] }
|
||||
if isLevel { return ["Steht eben"] }
|
||||
var parts: [String] = []
|
||||
if abs(pitch) > Self.levelTolerance {
|
||||
parts.append(pitch > 0 ? "Heck höher" : "Front höher")
|
||||
}
|
||||
if abs(roll) > Self.levelTolerance {
|
||||
parts.append(roll > 0 ? "Rechts höher" : "Links höher")
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func snapshot(deviceID: UUID, rssi: Int?) -> DeviceSnapshot {
|
||||
var snapshot = DeviceSnapshot(deviceID: deviceID, timestamp: Date(), rssi: rssi)
|
||||
snapshot.metrics = [
|
||||
|
||||
@@ -745,6 +745,177 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
AA0000000000000000000090 /* CarPlay */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CARPLAY_DEMO $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = CarPlay;
|
||||
};
|
||||
AA0000000000000000000091 /* CarPlay */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-App2-Testflight";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-App1-Testflight";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Config/VanControlCarPlay.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchsimulator*]" = "-";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = P8J6283TT5;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Config/VanControlCarPlay-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "VanControl CarPlay";
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth.";
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = NO;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControlCarPlay;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CARPLAY_DEMO $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = CarPlay;
|
||||
};
|
||||
AA0000000000000000000092 /* CarPlay */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchsimulator*]" = "-";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_TEAM = P8J6283TT5;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "VanControl CarPlay";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen des Neigungsmessers direkt an der Uhr, ohne Umweg ueber das iPhone.";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = de.s0.fototeddy.VanControlCarPlay;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControlCarPlay.watchkitapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "watchos watchsimulator";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = CarPlay;
|
||||
};
|
||||
AA0000000000000000000093 /* CarPlay */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
"CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchsimulator*]" = "-";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = P8J6283TT5;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Config/VanControlComplication-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Nivellierung;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControlCarPlay.watchkitapp.levelwidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = "watchos watchsimulator";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = CarPlay;
|
||||
};
|
||||
AA0000000000000000000094 /* CarPlay */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchsimulator*]" = "-";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = P8J6283TT5;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VanControlLiveActivity/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "VanControl Live Activity";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.s0.fototeddy.VanControlCarPlay.VanControlLiveActivity;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = CarPlay;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -752,6 +923,7 @@
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
AA0000000000000000000012 /* Debug */,
|
||||
AA0000000000000000000090 /* CarPlay */,
|
||||
AA0000000000000000000013 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
@@ -761,6 +933,7 @@
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
AA0000000000000000000014 /* Debug */,
|
||||
AA0000000000000000000091 /* CarPlay */,
|
||||
AA0000000000000000000015 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
@@ -770,6 +943,7 @@
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
AA0000000000000000000023 /* Debug */,
|
||||
AA0000000000000000000092 /* CarPlay */,
|
||||
AA0000000000000000000024 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
@@ -779,6 +953,7 @@
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
AA0000000000000000000038 /* Debug */,
|
||||
AA0000000000000000000093 /* CarPlay */,
|
||||
AA0000000000000000000039 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
@@ -788,6 +963,7 @@
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
AA0000000000000000000066 /* Debug */,
|
||||
AA0000000000000000000094 /* CarPlay */,
|
||||
AA0000000000000000000067 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2660"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "AA0000000000000000000006"
|
||||
BuildableName = "VanControl.app"
|
||||
BlueprintName = "VanControl"
|
||||
ReferencedContainer = "container:VanControl.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "CarPlay"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "CarPlay"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "AA0000000000000000000006"
|
||||
BuildableName = "VanControl.app"
|
||||
BlueprintName = "VanControl"
|
||||
ReferencedContainer = "container:VanControl.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "CARPLAY_DEMO_PITCH"
|
||||
value = "1.8"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "CARPLAY_DEMO_ROLL"
|
||||
value = "-0.9"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "CarPlay"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "AA0000000000000000000006"
|
||||
BuildableName = "VanControl.app"
|
||||
BlueprintName = "VanControl"
|
||||
ReferencedContainer = "container:VanControl.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "CarPlay">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "CarPlay"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,91 @@
|
||||
#if CARPLAY_DEMO
|
||||
import SwiftUI
|
||||
|
||||
/// Die Libelle auf dem Fahrzeugdisplay.
|
||||
///
|
||||
/// Quer geteilt: links die Blase, rechts in Worten, was zu tun ist. Beim
|
||||
/// Rangieren wird abwechselnd auf Display und Umfeld geschaut – deshalb steht
|
||||
/// die Aussage in einem Satz da, statt sich aus zwei Zahlen ergeben zu müssen.
|
||||
///
|
||||
/// Die Groessen sind auf die 400 × 240 **Punkte** ausgelegt, die CarPlay
|
||||
/// liefert – nicht auf die 800 × 480 Pixel. Das ist die Falle: In Pixeln
|
||||
/// gedachte Schriftgroessen sind auf dem Fahrzeugdisplay doppelt zu gross und
|
||||
/// schneiden den Text ab.
|
||||
struct CarPlayLevelScreen: View {
|
||||
@Environment(CarPlayLevelSource.self) private var source
|
||||
|
||||
private var state: LevelState {
|
||||
LevelState(pitch: source.pitch, roll: source.roll)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
if CarPlaySpeedGate.shared.isStationary {
|
||||
levelling
|
||||
} else {
|
||||
driving
|
||||
}
|
||||
}
|
||||
.onAppear { CarPlaySpeedGate.shared.start() }
|
||||
.onDisappear { CarPlaySpeedGate.shared.stop() }
|
||||
}
|
||||
|
||||
private var levelling: some View {
|
||||
HStack(spacing: 18) {
|
||||
LevelBubble(pitch: source.pitch, roll: source.roll)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
let lines = state.shortInstructions
|
||||
ForEach(lines.isEmpty ? ["Keine Verbindung"] : lines, id: \.self) { line in
|
||||
Text(line)
|
||||
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(state.isLevel ? .green : .white)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
tile("Längs", LevelDirectionFormatting.pitchTile(source.pitch))
|
||||
tile("Quer", LevelDirectionFormatting.rollTile(source.roll))
|
||||
}
|
||||
|
||||
if source.isSimulated {
|
||||
// Ohne diesen Hinweis wäre ein Screenshot aus dem Simulator
|
||||
// eine Behauptung über Messwerte, die es nicht gab.
|
||||
Text("Simulierte Werte")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
|
||||
private var driving: some View {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "car.fill")
|
||||
.font(.system(size: 30))
|
||||
Text("Nivellierung nur im Stand")
|
||||
.font(.system(size: 20, weight: .semibold, design: .rounded))
|
||||
}
|
||||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
|
||||
private func tile(_ label: String, _ value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(label)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.system(size: 22, weight: .semibold, design: .rounded))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,105 @@
|
||||
#if CARPLAY_DEMO
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
/// Woher die CarPlay-Libelle ihre Werte bekommt.
|
||||
///
|
||||
/// Im Simulator gibt es kein Bluetooth und damit keinen Neigungsmesser. Für
|
||||
/// die Demo läuft deshalb ein simulierter Verlauf: ein Fahrzeug, das langsam
|
||||
/// auf die Keile rollt, gerade wird und wieder abrutscht. Das reicht, um die
|
||||
/// Anzeige zu beurteilen – und für Screenshots, die zeigen, was die Funktion
|
||||
/// tut.
|
||||
///
|
||||
/// Für gezielte Aufnahmen lassen sich feste Werte über die Umgebung setzen:
|
||||
/// `CARPLAY_DEMO_PITCH` und `CARPLAY_DEMO_ROLL` (in Grad). Dann steht die
|
||||
/// Blase still und der Screenshot ist reproduzierbar.
|
||||
///
|
||||
/// Am Fahrzeug speist stattdessen der `BluetoothManager` über ``apply(_:)``
|
||||
/// ein; die Demo-Schleife läuft dann nicht.
|
||||
@Observable
|
||||
final class CarPlayLevelSource {
|
||||
static let shared = CarPlayLevelSource()
|
||||
|
||||
private(set) var pitch: Double?
|
||||
private(set) var roll: Double?
|
||||
|
||||
/// Ob die Werte simuliert sind. Die Anzeige weist darauf hin – ein
|
||||
/// Screenshot ohne diesen Hinweis wäre gegenüber Apple nicht ehrlich.
|
||||
private(set) var isSimulated = false
|
||||
|
||||
@ObservationIgnored private var timer: Timer?
|
||||
@ObservationIgnored private var phase: Double = 0
|
||||
@ObservationIgnored private lazy var isSequenced =
|
||||
ProcessInfo.processInfo.environment["CARPLAY_DEMO_SEQUENCE"] != nil
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Zehn Aktualisierungen pro Sekunde – schnell genug, dass die Blase der
|
||||
/// Bewegung des Fahrzeugs folgt statt ihr nachzulaufen.
|
||||
private static let updateInterval: TimeInterval = 0.1
|
||||
|
||||
func start() {
|
||||
guard timer == nil else { return }
|
||||
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
if let fixedPitch = environment["CARPLAY_DEMO_PITCH"].flatMap(Double.init),
|
||||
let fixedRoll = environment["CARPLAY_DEMO_ROLL"].flatMap(Double.init) {
|
||||
pitch = fixedPitch
|
||||
roll = fixedRoll
|
||||
isSimulated = true
|
||||
return
|
||||
}
|
||||
|
||||
isSimulated = true
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: Self.updateInterval, repeats: true) { [weak self] _ in
|
||||
self?.step()
|
||||
}
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
self.timer = timer
|
||||
}
|
||||
|
||||
func stop() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
/// Echte Messwerte vom Neigungsmesser. Sobald die kommen, ist nichts mehr
|
||||
/// simuliert.
|
||||
func apply(_ state: LevelState) {
|
||||
stop()
|
||||
isSimulated = false
|
||||
pitch = state.pitch
|
||||
roll = state.roll
|
||||
}
|
||||
|
||||
/// Feste Zustände im Wechsel, je sieben Sekunden – für Screenshots, die
|
||||
/// beide Fälle zuverlässig zeigen statt auf den richtigen Moment einer
|
||||
/// Schwingung zu warten.
|
||||
private static let sequence: [(pitch: Double, roll: Double)] = [
|
||||
(1.8, -0.9), // deutlich schief
|
||||
(0.2, -0.1), // ausgerichtet
|
||||
]
|
||||
|
||||
/// Ein Fahrzeug, das sich über gut zwanzig Sekunden einpendelt und wieder
|
||||
/// abweicht. Zwei unterschiedlich schnelle Schwingungen, damit Längs- und
|
||||
/// Querneigung nicht im Gleichtakt laufen – das sähe mechanisch aus.
|
||||
private func step() {
|
||||
phase += Self.updateInterval
|
||||
|
||||
if isSequenced {
|
||||
// Dritte Phase: die Sperre waehrend der Fahrt.
|
||||
let step = Int(phase / 7) % (Self.sequence.count + 1)
|
||||
if step == Self.sequence.count {
|
||||
CarPlaySpeedGate.shared.setDemoDriving(true)
|
||||
} else {
|
||||
CarPlaySpeedGate.shared.setDemoDriving(false)
|
||||
(pitch, roll) = Self.sequence[step]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pitch = 3.2 * sin(phase / 3.4)
|
||||
roll = 2.1 * sin(phase / 2.1 + 0.7)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,40 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Zeigt die CarPlay-Ansicht im iPhone-Simulator, in den Maßen, die CarPlay
|
||||
/// tatsächlich liefert (400 × 240 Punkte, also 800 × 480 Pixel).
|
||||
///
|
||||
/// Nur zum Beurteilen des Layouts, solange der CarPlay-Simulator selbst nicht
|
||||
/// zur Verfügung steht. Ohne `CARPLAY_DEMO_PREVIEW` in der Umgebung passiert
|
||||
/// nichts, und ohne das `CARPLAY_DEMO`-Flag existiert der Code gar nicht.
|
||||
extension View {
|
||||
@ViewBuilder
|
||||
func carPlayPreviewOverlay() -> some View {
|
||||
#if CARPLAY_DEMO
|
||||
if ProcessInfo.processInfo.environment["CARPLAY_DEMO_PREVIEW"] != nil {
|
||||
CarPlayPreviewHost()
|
||||
} else {
|
||||
self
|
||||
}
|
||||
#else
|
||||
self
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if CARPLAY_DEMO
|
||||
struct CarPlayPreviewHost: View {
|
||||
var body: some View {
|
||||
// Ohne Beiwerk und exakt zentriert: so laesst sich der Screenshot
|
||||
// verlustfrei auf den CarPlay-Bereich zuschneiden.
|
||||
VStack {
|
||||
CarPlayLevelScreen()
|
||||
.environment(CarPlayLevelSource.shared)
|
||||
.frame(width: 400, height: 240)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black)
|
||||
.ignoresSafeArea()
|
||||
.onAppear { CarPlayLevelSource.shared.start() }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,96 @@
|
||||
#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
|
||||
@@ -0,0 +1,77 @@
|
||||
#if CARPLAY_DEMO
|
||||
import CoreLocation
|
||||
import Observation
|
||||
|
||||
/// Sperrt die Libelle, sobald das Fahrzeug schneller als Schrittgeschwindigkeit
|
||||
/// fährt.
|
||||
///
|
||||
/// Ausrichten passiert im Stand oder im Zentimeterbereich davor. Eine Anzeige,
|
||||
/// die während der Fahrt etwas zu lesen gäbe, wäre Ablenkung ohne Nutzen – also
|
||||
/// gibt es sie dort nicht. Die Grenze ist bewusst technisch erzwungen und nicht
|
||||
/// bloss eine Zusage im Text.
|
||||
@Observable
|
||||
final class CarPlaySpeedGate: NSObject, CLLocationManagerDelegate {
|
||||
static let shared = CarPlaySpeedGate()
|
||||
|
||||
/// Fünf km/h: schneller als Rangieren, langsamer als jede Verkehrssituation,
|
||||
/// in der jemand aufs Display sehen dürfte.
|
||||
static let limit: CLLocationSpeed = 5.0 / 3.6
|
||||
|
||||
private(set) var isStationary = true
|
||||
private(set) var speed: CLLocationSpeed?
|
||||
|
||||
@ObservationIgnored private let manager = CLLocationManager()
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
manager.delegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
|
||||
}
|
||||
|
||||
func start() {
|
||||
// Fuer Screenshots laesst sich die Geschwindigkeit vorgeben (in km/h);
|
||||
// eine simulierte GPS-Fahrt nur fuer ein Standbild waere unverhaeltnis-
|
||||
// maessig. Ohne die Variable zaehlt ausschliesslich der echte Sensor.
|
||||
if let forced = ProcessInfo.processInfo.environment["CARPLAY_DEMO_SPEED"].flatMap(Double.init) {
|
||||
speed = forced / 3.6
|
||||
isStationary = forced / 3.6 <= Self.limit
|
||||
return
|
||||
}
|
||||
manager.requestWhenInUseAuthorization()
|
||||
manager.startUpdatingLocation()
|
||||
}
|
||||
|
||||
/// Nur für die Demo-Sequenz: erzwingt den Fahrzustand, damit sich die
|
||||
/// Sperre zeigen lässt, ohne eine GPS-Fahrt zu simulieren.
|
||||
func setDemoDriving(_ driving: Bool) {
|
||||
speed = driving ? 30 / 3.6 : 0
|
||||
isStationary = !driving
|
||||
}
|
||||
|
||||
func stop() {
|
||||
manager.stopUpdatingLocation()
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
|
||||
// CoreLocation meldet im Stand eine negative Geschwindigkeit: der Wert
|
||||
// ist dann schlicht unbekannt. Das als "fährt" zu werten, würde die
|
||||
// Funktion genau dort abschalten, wo sie gebraucht wird – auf dem
|
||||
// Stellplatz, oft unter Bäumen mit schlechtem Empfang.
|
||||
guard location.speed >= 0 else {
|
||||
speed = nil
|
||||
isStationary = true
|
||||
return
|
||||
}
|
||||
|
||||
speed = location.speed
|
||||
isStationary = location.speed <= Self.limit
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
speed = nil
|
||||
isStationary = true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -27,6 +27,7 @@ struct VanControlApp: App {
|
||||
.environment(watch)
|
||||
.environment(levelActivity)
|
||||
.task { watch.activate() }
|
||||
.carPlayPreviewOverlay()
|
||||
}
|
||||
.onChange(of: scenePhase) { _, phase in
|
||||
// Im Hintergrund darf ohne Service-Filter ohnehin nicht gescannt
|
||||
|
||||
@@ -75,9 +75,11 @@ struct LevelBubble: View {
|
||||
Circle()
|
||||
.fill(bubbleColor)
|
||||
.frame(width: bubble, height: bubble)
|
||||
// Positiver Pitch heisst: das Heck steht höher, die Blase
|
||||
// wandert also nach oben – in der Ansicht nach hinten.
|
||||
.offset(x: offsetX, y: -offsetY)
|
||||
// Draufsicht mit Fahrtrichtung oben: Positiver Pitch heisst,
|
||||
// das Heck steht höher – die Blase wandert nach hinten, also
|
||||
// nach unten. Wie bei einer echten Wasserwaage, die zur
|
||||
// höheren Seite ausschlägt.
|
||||
.offset(x: offsetX, y: offsetY)
|
||||
.animation(.spring(duration: 0.35), value: offsetX)
|
||||
.animation(.spring(duration: 0.35), value: offsetY)
|
||||
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
||||
|
||||
@@ -30,10 +30,11 @@ struct WatchBubble: View {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: bubble, height: bubble)
|
||||
// Positiver Pitch heisst: das Heck steht höher, die Blase
|
||||
// wandert nach oben – in der Ansicht also nach hinten.
|
||||
// Draufsicht mit Fahrtrichtung oben: Positiver Pitch heisst,
|
||||
// das Heck steht höher – die Blase wandert nach hinten, also
|
||||
// nach unten.
|
||||
.offset(x: clamped(roll) / range * travel,
|
||||
y: -clamped(pitch) / range * travel)
|
||||
y: clamped(pitch) / range * travel)
|
||||
.animation(.spring(duration: 0.3), value: pitch)
|
||||
.animation(.spring(duration: 0.3), value: roll)
|
||||
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
||||
|
||||
Reference in New Issue
Block a user