From a4b504c00d33b0694ed9332e0a6d13b50ac6b44d Mon Sep 17 00:00:00 2001 From: Justin Oros Date: Mon, 5 May 2025 09:33:00 -0700 Subject: [PATCH] GPS Speedometer app for iOS. Speedometer utilizing GPS. --- .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ Speedo/Assets.xcassets/Contents.json | 6 + Speedo/ContentView.swift | 133 +++++ Speedo/LocationManager.swift | 115 ++++ Speedo/Speedo.xcodeproj/project.pbxproj | 552 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 28022 bytes .../xcschemes/xcschememanagement.plist | 14 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ Speedo/Speedo/Assets.xcassets/Contents.json | 6 + Speedo/Speedo/ContentView.swift | 133 +++++ Speedo/Speedo/Info.plist | 13 + Speedo/Speedo/LocationManager.swift | 113 ++++ Speedo/Speedo/SpeedometerApp.swift | 17 + Speedo/SpeedoApp.swift | 17 + 17 files changed, 1218 insertions(+) create mode 100644 Speedo/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Speedo/Assets.xcassets/Contents.json create mode 100644 Speedo/ContentView.swift create mode 100644 Speedo/LocationManager.swift create mode 100644 Speedo/Speedo.xcodeproj/project.pbxproj create mode 100644 Speedo/Speedo.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Speedo/Speedo.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Speedo/Speedo.xcodeproj/xcuserdata/justinoros.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Speedo/Speedo/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Speedo/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Speedo/Speedo/Assets.xcassets/Contents.json create mode 100644 Speedo/Speedo/ContentView.swift create mode 100644 Speedo/Speedo/Info.plist create mode 100644 Speedo/Speedo/LocationManager.swift create mode 100644 Speedo/Speedo/SpeedometerApp.swift create mode 100644 Speedo/SpeedoApp.swift diff --git a/Speedo/Assets.xcassets/AccentColor.colorset/Contents.json b/Speedo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Speedo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/Assets.xcassets/Contents.json b/Speedo/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Speedo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/ContentView.swift b/Speedo/ContentView.swift new file mode 100644 index 0000000..a328f05 --- /dev/null +++ b/Speedo/ContentView.swift @@ -0,0 +1,133 @@ +// +// ContentView.swift +// Speedometer +// +// Created by Justin Oros on 5/2/2025. +// + +import SwiftUI +import CoreLocation + +struct ContentView: View { + @StateObject private var locationManager = LocationManager() + @State private var isMPH = true + @State private var previousLocation: CLLocation? + @State private var tripDistanceMeters: Double = 0.0 + + var currentSpeed: Int { + let multiplier = isMPH ? 1.0 : 1.60934 // Already in MPH from LocationManager + return Int(Double(locationManager.speed) * multiplier) + } + + var unitLabel: String { + isMPH ? "mph" : "kph" + } + + var distanceLabel: String { + let distance = isMPH ? tripDistanceMeters * 0.000621371 : tripDistanceMeters / 1000 + let roundedDistance = Int(distance.rounded()) + return "\(roundedDistance) \(isMPH ? "miles" : "km")" + } + + var accuracyColor: Color { + switch locationManager.accuracy { + case ..<10: + return .green + case 10..<30: + return .orange + default: + return .red + } + } + + var body: some View { + ZStack { + Color.black.ignoresSafeArea() + + VStack(spacing: 30) { + Spacer() + + // Center Speed Display with Unit + HStack(alignment: .bottom, spacing: 4) { + Text("\(currentSpeed)") + .font(.system(size: 140, weight: .bold, design: .rounded)) + .foregroundColor(.white) + + Text(unitLabel) + .font(.title2) + .foregroundColor(.gray) + .onTapGesture { + isMPH.toggle() + } + } + + // Trip Distance + Text("Trip: \(distanceLabel)") + .font(.title2) + .foregroundColor(.gray) + + // GPS Accuracy Display + Text(String(format: "GPS Accuracy: ±%.0f meters", locationManager.accuracy)) + .font(.headline) + .foregroundColor(accuracyColor) + + // Reset Button (smaller width) + Button(action: { + tripDistanceMeters = 0 + previousLocation = nil + }) { + Text("Reset Trip") + .font(.headline) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(Color.gray) + .foregroundColor(.black) + .cornerRadius(12) + } + + Spacer() + } + .padding() + + // Settings Button (Sprocket icon) at the bottom right with padding + VStack { + Spacer() + HStack { + Spacer() + Button(action: { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + }) { + Image(systemName: "gearshape.fill") // Sprocket icon + .font(.title2) + .foregroundColor(.white) + .padding() + } + .padding(.bottom, 10) // Add some bottom padding + } + .padding(.trailing) // Keep the trailing padding for right alignment + } + } + .alert(isPresented: $locationManager.showAlert) { + Alert( + title: Text("Location Services"), + message: Text(locationManager.alertMessage), + dismissButton: .default(Text("Settings")) { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + } + ) + } + .onReceive(locationManager.$location) { location in + if let newLocation = location { + if let oldLocation = previousLocation { + let distance = newLocation.distance(from: oldLocation) + tripDistanceMeters += distance + } + previousLocation = newLocation + } + } + } +} diff --git a/Speedo/LocationManager.swift b/Speedo/LocationManager.swift new file mode 100644 index 0000000..92b2eb5 --- /dev/null +++ b/Speedo/LocationManager.swift @@ -0,0 +1,115 @@ +// +// LocationManager.swift +// Speedometer +// +// Created by Justin Oros on 5/2/2025. +// + +import Foundation +import CoreLocation +import SwiftUI // Import SwiftUI for Alert + +class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { + private let manager = CLLocationManager() + + @Published var speed: Int = 0 // MPH as whole number + @Published var location: CLLocation? + @Published var accuracy: Double = 0.0 + @Published var authorizationStatus: CLAuthorizationStatus = .notDetermined + @Published var showAlert = false + @Published var alertMessage = "" + + override init() { + super.init() + manager.delegate = self + manager.activityType = .automotiveNavigation + manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation + manager.distanceFilter = 1 // Update on movement of 1 meter + print("LocationManager initialized") + requestAuthorization() // Request authorization in init + startUpdatingLocationIfNeeded() // Start updates if already authorized + } + + private func requestAuthorization() { + print("Requesting location authorization") + manager.requestAlwaysAuthorization() // Request "Always" permission + } + + private func startUpdatingLocationIfNeeded() { + if authorizationStatus == .authorizedAlways || authorizationStatus == .authorizedWhenInUse { + print("Attempting to start location updates") + manager.startUpdatingLocation() // Start receiving location updates + } else if authorizationStatus == .denied || authorizationStatus == .restricted { + print("Location updates not started due to denied/restricted authorization") + DispatchQueue.main.async { + self.showAlert = true + self.alertMessage = "Location services are disabled. Please enable them in Settings." + } + } else { + print("Authorization not yet determined, or other status: \(authorizationStatus)") + } + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + DispatchQueue.main.async { + self.authorizationStatus = status + print("Authorization status changed: \(status)") + self.startUpdatingLocationIfNeeded() // Check and start updates on authorization change + } + + switch status { + case .authorizedAlways: + print("Location authorization: Authorized Always") + case .authorizedWhenInUse: + print("Location authorization: Authorized When In Use") + case .denied: + print("Location authorization: Denied") + DispatchQueue.main.async { + self.showAlert = true + self.alertMessage = "Location access was denied. Please enable it in Settings for the app to function correctly." + } + case .restricted: + print("Location authorization: Restricted") + DispatchQueue.main.async { + self.showAlert = true + self.alertMessage = "Location access is restricted. This might be due to parental controls or system settings." + } + case .notDetermined: + print("Location authorization: Not Determined") + // Authorization request will be triggered in init + break + @unknown default: + print("Location authorization: Unknown status") + } + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let latest = locations.last else { + print("No latest location found in didUpdateLocations") + return + } + + // Debugging output + print("New location received: \(latest.coordinate.latitude), \(latest.coordinate.longitude)") + print("Speed (m/s): \(latest.speed)") + print("Speed (MPH): \(latest.speed * 2.23694)") + print("Accuracy: \(latest.horizontalAccuracy)") + + DispatchQueue.main.async { + // Update speed + let rawSpeed = max(latest.speed, 0) // Avoid negative values + let mph = rawSpeed * 2.23694 // Convert m/s to MPH + self.speed = Int(mph.rounded()) + self.location = latest + self.accuracy = latest.horizontalAccuracy + } + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("Location update failed: \(error.localizedDescription)") + DispatchQueue.main.async { + self.showAlert = true + self.alertMessage = "Location update failed: \(error.localizedDescription)" + } + } +} diff --git a/Speedo/Speedo.xcodeproj/project.pbxproj b/Speedo/Speedo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..30cbf77 --- /dev/null +++ b/Speedo/Speedo.xcodeproj/project.pbxproj @@ -0,0 +1,552 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + 97A3CA3D2DC90F75001ED670 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97A3CA272DC90F73001ED670 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97A3CA2E2DC90F73001ED670; + remoteInfo = Speedo; + }; + 97A3CA472DC90F75001ED670 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97A3CA272DC90F73001ED670 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97A3CA2E2DC90F73001ED670; + remoteInfo = Speedo; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 97A3CA2F2DC90F73001ED670 /* Speedo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Speedo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97A3CA3C2DC90F75001ED670 /* SpeedoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SpeedoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 97A3CA462DC90F75001ED670 /* SpeedoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SpeedoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 97E3710E2DC91223009146A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 97A3CA312DC90F73001ED670 /* Speedo */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Speedo; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97A3CA2C2DC90F73001ED670 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97A3CA392DC90F75001ED670 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97A3CA432DC90F75001ED670 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 97A3CA262DC90F73001ED670 = { + isa = PBXGroup; + children = ( + 97A3CA312DC90F73001ED670 /* Speedo */, + 97A3CA302DC90F73001ED670 /* Products */, + 97E3710F2DC91223009146A0 /* Info.plist */, + ); + sourceTree = ""; + }; + 97A3CA302DC90F73001ED670 /* Products */ = { + isa = PBXGroup; + children = ( + 97A3CA2F2DC90F73001ED670 /* Speedo.app */, + 97A3CA3C2DC90F75001ED670 /* SpeedoTests.xctest */, + 97A3CA462DC90F75001ED670 /* SpeedoUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97A3CA2E2DC90F73001ED670 /* Speedo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97A3CA502DC90F75001ED670 /* Build configuration list for PBXNativeTarget "Speedo" */; + buildPhases = ( + 97A3CA2B2DC90F73001ED670 /* Sources */, + 97A3CA2C2DC90F73001ED670 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 97A3CA312DC90F73001ED670 /* Speedo */, + ); + name = Speedo; + packageProductDependencies = ( + ); + productName = Speedo; + productReference = 97A3CA2F2DC90F73001ED670 /* Speedo.app */; + productType = "com.apple.product-type.application"; + }; + 97A3CA3B2DC90F75001ED670 /* SpeedoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97A3CA532DC90F75001ED670 /* Build configuration list for PBXNativeTarget "SpeedoTests" */; + buildPhases = ( + 97A3CA382DC90F75001ED670 /* Sources */, + 97A3CA392DC90F75001ED670 /* Frameworks */, + 97A3CA3A2DC90F75001ED670 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 97A3CA3E2DC90F75001ED670 /* PBXTargetDependency */, + ); + name = SpeedoTests; + packageProductDependencies = ( + ); + productName = SpeedoTests; + productReference = 97A3CA3C2DC90F75001ED670 /* SpeedoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97A3CA452DC90F75001ED670 /* SpeedoUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97A3CA562DC90F75001ED670 /* Build configuration list for PBXNativeTarget "SpeedoUITests" */; + buildPhases = ( + 97A3CA422DC90F75001ED670 /* Sources */, + 97A3CA432DC90F75001ED670 /* Frameworks */, + 97A3CA442DC90F75001ED670 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 97A3CA482DC90F75001ED670 /* PBXTargetDependency */, + ); + name = SpeedoUITests; + packageProductDependencies = ( + ); + productName = SpeedoUITests; + productReference = 97A3CA462DC90F75001ED670 /* SpeedoUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97A3CA272DC90F73001ED670 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 97A3CA2E2DC90F73001ED670 = { + CreatedOnToolsVersion = 16.3; + }; + 97A3CA3B2DC90F75001ED670 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = 97A3CA2E2DC90F73001ED670; + }; + 97A3CA452DC90F75001ED670 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = 97A3CA2E2DC90F73001ED670; + }; + }; + }; + buildConfigurationList = 97A3CA2A2DC90F73001ED670 /* Build configuration list for PBXProject "Speedo" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97A3CA262DC90F73001ED670; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 97A3CA302DC90F73001ED670 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97A3CA2E2DC90F73001ED670 /* Speedo */, + 97A3CA3B2DC90F75001ED670 /* SpeedoTests */, + 97A3CA452DC90F75001ED670 /* SpeedoUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97A3CA3A2DC90F75001ED670 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97A3CA442DC90F75001ED670 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97A3CA2B2DC90F73001ED670 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97A3CA382DC90F75001ED670 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97A3CA422DC90F75001ED670 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 97A3CA3E2DC90F75001ED670 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97A3CA2E2DC90F73001ED670 /* Speedo */; + targetProxy = 97A3CA3D2DC90F75001ED670 /* PBXContainerItemProxy */; + }; + 97A3CA482DC90F75001ED670 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97A3CA2E2DC90F73001ED670 /* Speedo */; + targetProxy = 97A3CA472DC90F75001ED670 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97E3710F2DC91223009146A0 /* Info.plist */ = { + isa = PBXVariantGroup; + children = ( + 97E3710E2DC91223009146A0 /* Base */, + ); + name = Info.plist; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97A3CA4E2DC90F75001ED670 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + 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)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 97A3CA4F2DC90F75001ED670 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97A3CA512DC90F75001ED670 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Speedo/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + 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 = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.justinoros.Speedo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97A3CA522DC90F75001ED670 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Speedo/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + 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 = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.justinoros.Speedo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 97A3CA542DC90F75001ED670 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.justinoros.SpeedoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Speedo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Speedo"; + }; + name = Debug; + }; + 97A3CA552DC90F75001ED670 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.justinoros.SpeedoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Speedo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Speedo"; + }; + name = Release; + }; + 97A3CA572DC90F75001ED670 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.justinoros.SpeedoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Speedo; + }; + name = Debug; + }; + 97A3CA582DC90F75001ED670 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5RZLK3W7ZH; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.justinoros.SpeedoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Speedo; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97A3CA2A2DC90F73001ED670 /* Build configuration list for PBXProject "Speedo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97A3CA4E2DC90F75001ED670 /* Debug */, + 97A3CA4F2DC90F75001ED670 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97A3CA502DC90F75001ED670 /* Build configuration list for PBXNativeTarget "Speedo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97A3CA512DC90F75001ED670 /* Debug */, + 97A3CA522DC90F75001ED670 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97A3CA532DC90F75001ED670 /* Build configuration list for PBXNativeTarget "SpeedoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97A3CA542DC90F75001ED670 /* Debug */, + 97A3CA552DC90F75001ED670 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97A3CA562DC90F75001ED670 /* Build configuration list for PBXNativeTarget "SpeedoUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97A3CA572DC90F75001ED670 /* Debug */, + 97A3CA582DC90F75001ED670 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97A3CA272DC90F73001ED670 /* Project object */; +} diff --git a/Speedo/Speedo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Speedo/Speedo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Speedo/Speedo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Speedo/Speedo.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate b/Speedo/Speedo.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..987f986f2e6549a038f472dd1044c1d884bef20e GIT binary patch literal 28022 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 literal 0 HcmV?d00001 diff --git a/Speedo/Speedo.xcodeproj/xcuserdata/justinoros.xcuserdatad/xcschemes/xcschememanagement.plist b/Speedo/Speedo.xcodeproj/xcuserdata/justinoros.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..f1e753c --- /dev/null +++ b/Speedo/Speedo.xcodeproj/xcuserdata/justinoros.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Speedo.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/Speedo/Speedo/Assets.xcassets/AccentColor.colorset/Contents.json b/Speedo/Speedo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Speedo/Speedo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Speedo/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Speedo/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/Speedo/Assets.xcassets/Contents.json b/Speedo/Speedo/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Speedo/Speedo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Speedo/Speedo/ContentView.swift b/Speedo/Speedo/ContentView.swift new file mode 100644 index 0000000..a328f05 --- /dev/null +++ b/Speedo/Speedo/ContentView.swift @@ -0,0 +1,133 @@ +// +// ContentView.swift +// Speedometer +// +// Created by Justin Oros on 5/2/2025. +// + +import SwiftUI +import CoreLocation + +struct ContentView: View { + @StateObject private var locationManager = LocationManager() + @State private var isMPH = true + @State private var previousLocation: CLLocation? + @State private var tripDistanceMeters: Double = 0.0 + + var currentSpeed: Int { + let multiplier = isMPH ? 1.0 : 1.60934 // Already in MPH from LocationManager + return Int(Double(locationManager.speed) * multiplier) + } + + var unitLabel: String { + isMPH ? "mph" : "kph" + } + + var distanceLabel: String { + let distance = isMPH ? tripDistanceMeters * 0.000621371 : tripDistanceMeters / 1000 + let roundedDistance = Int(distance.rounded()) + return "\(roundedDistance) \(isMPH ? "miles" : "km")" + } + + var accuracyColor: Color { + switch locationManager.accuracy { + case ..<10: + return .green + case 10..<30: + return .orange + default: + return .red + } + } + + var body: some View { + ZStack { + Color.black.ignoresSafeArea() + + VStack(spacing: 30) { + Spacer() + + // Center Speed Display with Unit + HStack(alignment: .bottom, spacing: 4) { + Text("\(currentSpeed)") + .font(.system(size: 140, weight: .bold, design: .rounded)) + .foregroundColor(.white) + + Text(unitLabel) + .font(.title2) + .foregroundColor(.gray) + .onTapGesture { + isMPH.toggle() + } + } + + // Trip Distance + Text("Trip: \(distanceLabel)") + .font(.title2) + .foregroundColor(.gray) + + // GPS Accuracy Display + Text(String(format: "GPS Accuracy: ±%.0f meters", locationManager.accuracy)) + .font(.headline) + .foregroundColor(accuracyColor) + + // Reset Button (smaller width) + Button(action: { + tripDistanceMeters = 0 + previousLocation = nil + }) { + Text("Reset Trip") + .font(.headline) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(Color.gray) + .foregroundColor(.black) + .cornerRadius(12) + } + + Spacer() + } + .padding() + + // Settings Button (Sprocket icon) at the bottom right with padding + VStack { + Spacer() + HStack { + Spacer() + Button(action: { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + }) { + Image(systemName: "gearshape.fill") // Sprocket icon + .font(.title2) + .foregroundColor(.white) + .padding() + } + .padding(.bottom, 10) // Add some bottom padding + } + .padding(.trailing) // Keep the trailing padding for right alignment + } + } + .alert(isPresented: $locationManager.showAlert) { + Alert( + title: Text("Location Services"), + message: Text(locationManager.alertMessage), + dismissButton: .default(Text("Settings")) { + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url) + } + } + ) + } + .onReceive(locationManager.$location) { location in + if let newLocation = location { + if let oldLocation = previousLocation { + let distance = newLocation.distance(from: oldLocation) + tripDistanceMeters += distance + } + previousLocation = newLocation + } + } + } +} diff --git a/Speedo/Speedo/Info.plist b/Speedo/Speedo/Info.plist new file mode 100644 index 0000000..7ef4d66 --- /dev/null +++ b/Speedo/Speedo/Info.plist @@ -0,0 +1,13 @@ + + + + + + NSLocationWhenInUseUsageDescription + This app needs your location to calculate speed while you're using it. + NSLocationAlwaysAndWhenInUseUsageDescription + Location is used to calculate your current speed in real time. + + + diff --git a/Speedo/Speedo/LocationManager.swift b/Speedo/Speedo/LocationManager.swift new file mode 100644 index 0000000..34f825a --- /dev/null +++ b/Speedo/Speedo/LocationManager.swift @@ -0,0 +1,113 @@ +// +// LocationManager.swift +// Speedometer +// +// Created by Justin Oros on 5/2/2025. +// + +import Foundation +import CoreLocation +import SwiftUI + +class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { + private let manager = CLLocationManager() + + @Published var speed: Int = 0 + @Published var location: CLLocation? + @Published var accuracy: Double = 0.0 + @Published var authorizationStatus: CLAuthorizationStatus = .notDetermined + @Published var showAlert = false + @Published var alertMessage = "" + + override init() { + super.init() + manager.delegate = self + manager.activityType = .automotiveNavigation + manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation + manager.distanceFilter = 1 + print("LocationManager initialized") + requestAuthorization() + startUpdatingLocationIfNeeded() + } + + private func requestAuthorization() { + print("Requesting location authorization") + manager.requestWhenInUseAuthorization() + } + + private func startUpdatingLocationIfNeeded() { + if authorizationStatus == .authorizedAlways || authorizationStatus == .authorizedWhenInUse { + print("Attempting to start location updates") + manager.startUpdatingLocation() + } else if authorizationStatus == .denied || authorizationStatus == .restricted { + print("Location updates not started due to denied/restricted authorization") + DispatchQueue.main.async { + self.showAlert = true + self.alertMessage = "Location services are disabled. Please enable them in Settings." + } + } else { + print("Authorization not yet determined: \(authorizationStatus.rawValue)") + } + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + let status = manager.authorizationStatus + DispatchQueue.main.async { + self.authorizationStatus = status + print("Authorization status changed: \(status.rawValue)") + self.startUpdatingLocationIfNeeded() + } + + switch status { + case .authorizedAlways: + print("Location authorization: Authorized Always") + case .authorizedWhenInUse: + print("Location authorization: Authorized When In Use") + case .denied: + print("Location authorization: Denied") + DispatchQueue.main.async { + self.showAlert = true + self.alertMessage = "Location access was denied. Please enable it in Settings for the app to function correctly." + } + case .restricted: + print("Location authorization: Restricted") + DispatchQueue.main.async { + self.showAlert = true + self.alertMessage = "Location access is restricted. This might be due to parental controls or system settings." + } + case .notDetermined: + print("Location authorization: Not Determined") + @unknown default: + print("Location authorization: Unknown status") + } + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let latest = locations.last else { + print("No latest location found") + return + } + + print("New location: \(latest.coordinate.latitude), \(latest.coordinate.longitude)") + print("Speed (m/s): \(latest.speed)") + print("Speed (MPH): \(latest.speed * 2.23694)") + print("Accuracy: \(latest.horizontalAccuracy)") + + DispatchQueue.main.async { + let rawSpeed = max(latest.speed, 0) + let mph = rawSpeed * 2.23694 + self.speed = Int(mph.rounded()) + self.location = latest + self.accuracy = latest.horizontalAccuracy + } + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("Location update failed: \(error.localizedDescription)") + DispatchQueue.main.async { + self.showAlert = true + self.alertMessage = "Location update failed: \(error.localizedDescription)" + } + } +} + diff --git a/Speedo/Speedo/SpeedometerApp.swift b/Speedo/Speedo/SpeedometerApp.swift new file mode 100644 index 0000000..9d3ed95 --- /dev/null +++ b/Speedo/Speedo/SpeedometerApp.swift @@ -0,0 +1,17 @@ +// +// SpeedometerApp.swift +// Speedometer +// +// Created by Justin Oros on 5/2/2025. +// + +import SwiftUI + +@main +struct SpeedometerApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Speedo/SpeedoApp.swift b/Speedo/SpeedoApp.swift new file mode 100644 index 0000000..9d3ed95 --- /dev/null +++ b/Speedo/SpeedoApp.swift @@ -0,0 +1,17 @@ +// +// SpeedometerApp.swift +// Speedometer +// +// Created by Justin Oros on 5/2/2025. +// + +import SwiftUI + +@main +struct SpeedometerApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +}