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: ", ")
|
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 {
|
func snapshot(deviceID: UUID, rssi: Int?) -> DeviceSnapshot {
|
||||||
var snapshot = DeviceSnapshot(deviceID: deviceID, timestamp: Date(), rssi: rssi)
|
var snapshot = DeviceSnapshot(deviceID: deviceID, timestamp: Date(), rssi: rssi)
|
||||||
snapshot.metrics = [
|
snapshot.metrics = [
|
||||||
|
|||||||
@@ -745,6 +745,177 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
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 */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -752,6 +923,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
AA0000000000000000000012 /* Debug */,
|
AA0000000000000000000012 /* Debug */,
|
||||||
|
AA0000000000000000000090 /* CarPlay */,
|
||||||
AA0000000000000000000013 /* Release */,
|
AA0000000000000000000013 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@@ -761,6 +933,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
AA0000000000000000000014 /* Debug */,
|
AA0000000000000000000014 /* Debug */,
|
||||||
|
AA0000000000000000000091 /* CarPlay */,
|
||||||
AA0000000000000000000015 /* Release */,
|
AA0000000000000000000015 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@@ -770,6 +943,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
AA0000000000000000000023 /* Debug */,
|
AA0000000000000000000023 /* Debug */,
|
||||||
|
AA0000000000000000000092 /* CarPlay */,
|
||||||
AA0000000000000000000024 /* Release */,
|
AA0000000000000000000024 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@@ -779,6 +953,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
AA0000000000000000000038 /* Debug */,
|
AA0000000000000000000038 /* Debug */,
|
||||||
|
AA0000000000000000000093 /* CarPlay */,
|
||||||
AA0000000000000000000039 /* Release */,
|
AA0000000000000000000039 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@@ -788,6 +963,7 @@
|
|||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
AA0000000000000000000066 /* Debug */,
|
AA0000000000000000000066 /* Debug */,
|
||||||
|
AA0000000000000000000094 /* CarPlay */,
|
||||||
AA0000000000000000000067 /* Release */,
|
AA0000000000000000000067 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
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(watch)
|
||||||
.environment(levelActivity)
|
.environment(levelActivity)
|
||||||
.task { watch.activate() }
|
.task { watch.activate() }
|
||||||
|
.carPlayPreviewOverlay()
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { _, phase in
|
.onChange(of: scenePhase) { _, phase in
|
||||||
// Im Hintergrund darf ohne Service-Filter ohnehin nicht gescannt
|
// Im Hintergrund darf ohne Service-Filter ohnehin nicht gescannt
|
||||||
|
|||||||
@@ -75,9 +75,11 @@ struct LevelBubble: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(bubbleColor)
|
.fill(bubbleColor)
|
||||||
.frame(width: bubble, height: bubble)
|
.frame(width: bubble, height: bubble)
|
||||||
// Positiver Pitch heisst: das Heck steht höher, die Blase
|
// Draufsicht mit Fahrtrichtung oben: Positiver Pitch heisst,
|
||||||
// wandert also nach oben – in der Ansicht nach hinten.
|
// das Heck steht höher – die Blase wandert nach hinten, also
|
||||||
.offset(x: offsetX, y: -offsetY)
|
// 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: offsetX)
|
||||||
.animation(.spring(duration: 0.35), value: offsetY)
|
.animation(.spring(duration: 0.35), value: offsetY)
|
||||||
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ struct WatchBubble: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(color)
|
.fill(color)
|
||||||
.frame(width: bubble, height: bubble)
|
.frame(width: bubble, height: bubble)
|
||||||
// Positiver Pitch heisst: das Heck steht höher, die Blase
|
// Draufsicht mit Fahrtrichtung oben: Positiver Pitch heisst,
|
||||||
// wandert nach oben – in der Ansicht also nach hinten.
|
// das Heck steht höher – die Blase wandert nach hinten, also
|
||||||
|
// nach unten.
|
||||||
.offset(x: clamped(roll) / range * travel,
|
.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: pitch)
|
||||||
.animation(.spring(duration: 0.3), value: roll)
|
.animation(.spring(duration: 0.3), value: roll)
|
||||||
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user