diff --git a/Speedo/Assets.xcassets/AppIcon.appiconset/1024.png b/Speedo/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..556abbe Binary files /dev/null and b/Speedo/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..9803402 100644 --- a/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Speedo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/Speedo/Speedo.xcodeproj/project.pbxproj b/Speedo/Speedo.xcodeproj/project.pbxproj index 30cbf77..b237728 100644 --- a/Speedo/Speedo.xcodeproj/project.pbxproj +++ b/Speedo/Speedo.xcodeproj/project.pbxproj @@ -6,33 +6,31 @@ 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 PBXBuildFile section */ + 97DBB1252DCC4CDC0027503D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97DBB0D32DCB175B0027503D /* Assets.xcassets */; }; +/* End PBXBuildFile 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 = ""; }; + 97DBB0D32DCB175B0027503D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 97DBB1282DCC4D870027503D /* Exceptions for "Speedo" folder in "Speedo" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 97A3CA2E2DC90F73001ED670 /* Speedo */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 97A3CA312DC90F73001ED670 /* Speedo */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 97DBB1282DCC4D870027503D /* Exceptions for "Speedo" folder in "Speedo" target */, + ); path = Speedo; sourceTree = ""; }; @@ -46,20 +44,6 @@ ); 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 */ @@ -68,7 +52,7 @@ children = ( 97A3CA312DC90F73001ED670 /* Speedo */, 97A3CA302DC90F73001ED670 /* Products */, - 97E3710F2DC91223009146A0 /* Info.plist */, + 97DBB0D32DCB175B0027503D /* Assets.xcassets */, ); sourceTree = ""; }; @@ -76,8 +60,6 @@ isa = PBXGroup; children = ( 97A3CA2F2DC90F73001ED670 /* Speedo.app */, - 97A3CA3C2DC90F75001ED670 /* SpeedoTests.xctest */, - 97A3CA462DC90F75001ED670 /* SpeedoUITests.xctest */, ); name = Products; sourceTree = ""; @@ -91,6 +73,7 @@ buildPhases = ( 97A3CA2B2DC90F73001ED670 /* Sources */, 97A3CA2C2DC90F73001ED670 /* Frameworks */, + 97DBB1232DCC4CD20027503D /* Resources */, ); buildRules = ( ); @@ -106,46 +89,6 @@ 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 */ @@ -159,14 +102,6 @@ 97A3CA2E2DC90F73001ED670 = { CreatedOnToolsVersion = 16.3; }; - 97A3CA3B2DC90F75001ED670 = { - CreatedOnToolsVersion = 16.3; - TestTargetID = 97A3CA2E2DC90F73001ED670; - }; - 97A3CA452DC90F75001ED670 = { - CreatedOnToolsVersion = 16.3; - TestTargetID = 97A3CA2E2DC90F73001ED670; - }; }; }; buildConfigurationList = 97A3CA2A2DC90F73001ED670 /* Build configuration list for PBXProject "Speedo" */; @@ -184,24 +119,16 @@ 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 */ = { + 97DBB1232DCC4CD20027503D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 97DBB1252DCC4CDC0027503D /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -215,46 +142,8 @@ ); 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; @@ -382,6 +271,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 5RZLK3W7ZH; @@ -400,6 +290,10 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.justinoros.Speedo; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -411,6 +305,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 5RZLK3W7ZH; @@ -429,84 +324,16 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.justinoros.Speedo; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 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 */ @@ -528,24 +355,6 @@ 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/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate b/Speedo/Speedo.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate index 987f986..6e326ec 100644 Binary files a/Speedo/Speedo.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate and b/Speedo/Speedo.xcodeproj/project.xcworkspace/xcuserdata/justinoros.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Speedo/Speedo/ContentView.swift b/Speedo/Speedo/ContentView.swift index a328f05..aaf73e4 100644 --- a/Speedo/Speedo/ContentView.swift +++ b/Speedo/Speedo/ContentView.swift @@ -1,114 +1,147 @@ -// -// ContentView.swift -// Speedometer -// -// Created by Justin Oros on 5/2/2025. -// +// Speedometer App for iOS +// Author: Justin Oros +// ContentView.swift 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 + // App storage settings + @AppStorage("SpeedUnitIsMPH") private var isMPH: Bool = true + @AppStorage("DarkModeEnabled") private var isDarkMode: Bool = true // Default to dark mode + @AppStorage("MinimalViewEnabled") private var isMinimalView: Bool = false + @AppStorage("TripDistance") private var tripDistanceMeters: Double = 0.0 + + @State private var previousLocation: CLLocation? + + // Converts speed in m/s to selected unit var currentSpeed: Int { - let multiplier = isMPH ? 1.0 : 1.60934 // Already in MPH from LocationManager - return Int(Double(locationManager.speed) * multiplier) + let speedMS = locationManager.speedInMS + let converted = isMPH ? speedMS * 2.23694 : speedMS * 3.6 + return Int(converted.rounded()) } + // Returns unit label string var unitLabel: String { isMPH ? "mph" : "kph" } + // Converts and formats trip distance string var distanceLabel: String { let distance = isMPH ? tripDistanceMeters * 0.000621371 : tripDistanceMeters / 1000 - let roundedDistance = Int(distance.rounded()) - return "\(roundedDistance) \(isMPH ? "miles" : "km")" + return "\(Int(distance.rounded())) \(isMPH ? "miles" : "km")" } + // Determines color based on GPS accuracy var accuracyColor: Color { switch locationManager.accuracy { - case ..<10: - return .green - case 10..<30: - return .orange - default: - return .red + case ..<10: return .green + case 10..<30: return .orange + default: return .red } } + // UI colors + var foregroundColor: Color { + isDarkMode ? .white : .black + } + + var secondaryTextColor: Color { + .gray + } + + var backgroundColor: Color { + isDarkMode ? .black : .white + } + var body: some View { ZStack { - Color.black.ignoresSafeArea() + backgroundColor.ignoresSafeArea() VStack(spacing: 30) { Spacer() - // Center Speed Display with Unit + // Main speed display HStack(alignment: .bottom, spacing: 4) { Text("\(currentSpeed)") .font(.system(size: 140, weight: .bold, design: .rounded)) - .foregroundColor(.white) + .foregroundColor(foregroundColor) Text(unitLabel) .font(.title2) - .foregroundColor(.gray) + .foregroundColor(secondaryTextColor) .onTapGesture { isMPH.toggle() } } - // Trip Distance - Text("Trip: \(distanceLabel)") - .font(.title2) - .foregroundColor(.gray) + if !isMinimalView { + // Trip distance and reset button + HStack(spacing: 8) { + Text("Trip: \(distanceLabel)") + .font(.title2) + .foregroundColor(secondaryTextColor) - // GPS Accuracy Display - Text(String(format: "GPS Accuracy: ±%.0f meters", locationManager.accuracy)) - .font(.headline) - .foregroundColor(accuracyColor) + Button(action: { + tripDistanceMeters = 0 + previousLocation = nil + }) { + Image(systemName: "arrow.counterclockwise.circle") + .font(.title2) + .foregroundColor(secondaryTextColor) + } + .buttonStyle(PlainButtonStyle()) + } - // Reset Button (smaller width) - Button(action: { - tripDistanceMeters = 0 - previousLocation = nil - }) { - Text("Reset Trip") + // GPS accuracy display + Text(String(format: "GPS Accuracy: ±%.0f meters", locationManager.accuracy)) .font(.headline) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .background(Color.gray) - .foregroundColor(.black) - .cornerRadius(12) + .foregroundColor(accuracyColor) + } else if locationManager.accuracy == 0 { + Text("Waiting for GPS...") + .foregroundColor(.gray) } Spacer() } .padding() - // Settings Button (Sprocket icon) at the bottom right with padding + // Toggle buttons VStack { Spacer() HStack { + // Toggle minimal/full view + Button(action: { + isMinimalView.toggle() + }) { + Image(systemName: isMinimalView ? "eye.slash.fill" : "eye.fill") + .font(.title2) + .foregroundColor(foregroundColor) + .padding() + } + Spacer() + + // Open app settings Button(action: { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } }) { - Image(systemName: "gearshape.fill") // Sprocket icon + Image(systemName: "gearshape.fill") .font(.title2) - .foregroundColor(.white) + .foregroundColor(foregroundColor) .padding() } - .padding(.bottom, 10) // Add some bottom padding } - .padding(.trailing) // Keep the trailing padding for right alignment + .padding([.leading, .trailing, .bottom], 10) } } + .onTapGesture { + isDarkMode.toggle() + } .alert(isPresented: $locationManager.showAlert) { Alert( title: Text("Location Services"), @@ -120,8 +153,11 @@ struct ContentView: View { } ) } + // Update trip distance with each valid location update .onReceive(locationManager.$location) { location in - if let newLocation = location { + if let newLocation = location, + newLocation.speed > 0.5, + newLocation.horizontalAccuracy < 20 { if let oldLocation = previousLocation { let distance = newLocation.distance(from: oldLocation) tripDistanceMeters += distance @@ -131,3 +167,4 @@ struct ContentView: View { } } } + diff --git a/Speedo/Speedo/LocationManager.swift b/Speedo/Speedo/LocationManager.swift index 34f825a..a7fbc31 100644 --- a/Speedo/Speedo/LocationManager.swift +++ b/Speedo/Speedo/LocationManager.swift @@ -1,9 +1,6 @@ -// -// LocationManager.swift -// Speedometer -// -// Created by Justin Oros on 5/2/2025. -// +// Speedometer App for iOS +// Author: Justin Oros +// LocationManager.swift import Foundation import CoreLocation @@ -12,7 +9,7 @@ import SwiftUI class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { private let manager = CLLocationManager() - @Published var speed: Int = 0 + @Published var speedInMS: Double = 0.0 @Published var location: CLLocation? @Published var accuracy: Double = 0.0 @Published var authorizationStatus: CLAuthorizationStatus = .notDetermined @@ -25,89 +22,80 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { manager.activityType = .automotiveNavigation manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation manager.distanceFilter = 1 - print("LocationManager initialized") + requestAuthorization() startUpdatingLocationIfNeeded() } + // Request location access permission private func requestAuthorization() { - print("Requesting location authorization") manager.requestWhenInUseAuthorization() } + // Begin location updates if permission is granted 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)") } } + // Handle changes in authorization status 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." + self.alertMessage = "Location access was denied. Please enable it in Settings." } 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." + self.alertMessage = "Location access is restricted." } - case .notDetermined: - print("Location authorization: Not Determined") - @unknown default: - print("Location authorization: Unknown status") + default: break } } + // Handle new location data 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)") + guard let latest = locations.last, + latest.horizontalAccuracy >= 0, + latest.horizontalAccuracy < 20 else { return } DispatchQueue.main.async { let rawSpeed = max(latest.speed, 0) - let mph = rawSpeed * 2.23694 - self.speed = Int(mph.rounded()) + self.speedInMS = rawSpeed < 0.5 ? 0 : rawSpeed self.location = latest self.accuracy = latest.horizontalAccuracy } } + // Handle location update errors func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - print("Location update failed: \(error.localizedDescription)") + let nsError = error as NSError + + // Ignore transient or non-critical errors + if nsError.code == CLError.locationUnknown.rawValue || + nsError.code == CLError.network.rawValue || + nsError.code == CLError.denied.rawValue || + nsError.code == CLError.deferredFailed.rawValue { + return + } + 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 index 9d3ed95..e863900 100644 --- a/Speedo/Speedo/SpeedometerApp.swift +++ b/Speedo/Speedo/SpeedometerApp.swift @@ -1,9 +1,6 @@ -// +// Speedometer App for iOS +// Author: Justin Oros // SpeedometerApp.swift -// Speedometer -// -// Created by Justin Oros on 5/2/2025. -// import SwiftUI