Files
Swift/Speedo/ContentView.swift
T

185 lines
6.2 KiB
Swift
Raw Normal View History

2025-05-05 09:33:00 -07:00
//
// ContentView.swift
// Speedometer
//
// Created by Justin Oros on 5/2/2025.
//
import SwiftUI
import CoreLocation
struct ContentView: View {
2025-05-05 18:44:20 -07:00
@StateObject private var locationManager = LocationManager() // Manages live GPS data
@State private var isMPH = true // Toggle between MPH and KPH
2025-05-05 09:33:00 -07:00
@State private var previousLocation: CLLocation?
2025-05-05 18:44:20 -07:00
@State private var tripDistanceMeters: Double = 0.0 // Trip distance in meters
@State private var isDarkMode = true // Theme toggle: dark/light
@State private var isMinimalView = false // Toggle minimal UI showing only speed
2025-05-05 09:33:00 -07:00
2025-05-05 18:44:20 -07:00
// Load saved trip distance when the view is initialized (even before body runs)
init() {
if let savedDistance = UserDefaults.standard.object(forKey: "TripDistance") as? Double {
_tripDistanceMeters = State(initialValue: savedDistance)
}
}
// Current speed converted to either MPH or KPH
2025-05-05 09:33:00 -07:00
var currentSpeed: Int {
2025-05-05 10:02:55 -07:00
let multiplier = isMPH ? 1.0 : 1.60934
2025-05-05 09:33:00 -07:00
return Int(Double(locationManager.speed) * multiplier)
}
2025-05-05 18:44:20 -07:00
// Display correct unit
2025-05-05 09:33:00 -07:00
var unitLabel: String {
isMPH ? "mph" : "kph"
}
2025-05-05 18:44:20 -07:00
// Display formatted trip distance
2025-05-05 09:33:00 -07:00
var distanceLabel: String {
let distance = isMPH ? tripDistanceMeters * 0.000621371 : tripDistanceMeters / 1000
let roundedDistance = Int(distance.rounded())
return "\(roundedDistance) \(isMPH ? "miles" : "km")"
}
2025-05-05 18:44:20 -07:00
// Color based on GPS accuracy
2025-05-05 09:33:00 -07:00
var accuracyColor: Color {
switch locationManager.accuracy {
case ..<10:
return .green
case 10..<30:
return .orange
default:
return .red
}
}
2025-05-05 18:44:20 -07:00
// Theme-based color helpers
2025-05-05 10:02:55 -07:00
var foregroundColor: Color {
isDarkMode ? .white : .black
}
var secondaryTextColor: Color {
isDarkMode ? .gray : .gray
}
var backgroundColor: Color {
isDarkMode ? .black : .white
}
2025-05-05 09:33:00 -07:00
var body: some View {
ZStack {
2025-05-05 10:02:55 -07:00
backgroundColor.ignoresSafeArea()
2025-05-05 09:33:00 -07:00
VStack(spacing: 30) {
Spacer()
2025-05-05 18:44:20 -07:00
// Speed value and unit
2025-05-05 09:33:00 -07:00
HStack(alignment: .bottom, spacing: 4) {
Text("\(currentSpeed)")
.font(.system(size: 140, weight: .bold, design: .rounded))
2025-05-05 10:02:55 -07:00
.foregroundColor(foregroundColor)
2025-05-05 09:33:00 -07:00
Text(unitLabel)
.font(.title2)
2025-05-05 10:02:55 -07:00
.foregroundColor(secondaryTextColor)
2025-05-05 09:33:00 -07:00
.onTapGesture {
2025-05-05 18:44:20 -07:00
isMPH.toggle() // Tap to switch between MPH/KPH
2025-05-05 09:33:00 -07:00
}
}
2025-05-05 18:44:20 -07:00
// Extra details (hidden in minimal mode)
2025-05-05 17:44:04 -07:00
if !isMinimalView {
Text("Trip: \(distanceLabel)")
.font(.title2)
.foregroundColor(secondaryTextColor)
2025-05-05 09:33:00 -07:00
2025-05-05 17:44:04 -07:00
Text(String(format: "GPS Accuracy: ±%.0f meters", locationManager.accuracy))
2025-05-05 09:33:00 -07:00
.font(.headline)
2025-05-05 17:44:04 -07:00
.foregroundColor(accuracyColor)
2025-05-05 18:44:20 -07:00
// Reset trip button clears distance and saved value
2025-05-05 17:44:04 -07:00
Button(action: {
tripDistanceMeters = 0
previousLocation = nil
2025-05-05 18:44:20 -07:00
UserDefaults.standard.set(0.0, forKey: "TripDistance") // Clear persistence
2025-05-05 17:44:04 -07:00
}) {
Text("Reset Trip")
.font(.headline)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(secondaryTextColor)
.foregroundColor(backgroundColor)
.cornerRadius(12)
}
2025-05-05 09:33:00 -07:00
}
Spacer()
}
.padding()
2025-05-05 18:44:20 -07:00
// Bottom control buttons
2025-05-05 09:33:00 -07:00
VStack {
Spacer()
HStack {
2025-05-05 18:44:20 -07:00
// Toggle minimal/full view
2025-05-05 17:44:04 -07:00
Button(action: {
isMinimalView.toggle()
}) {
Image(systemName: isMinimalView ? "eye.slash.fill" : "eye.fill")
.font(.title2)
.foregroundColor(foregroundColor)
.padding()
}
2025-05-05 09:33:00 -07:00
Spacer()
2025-05-05 17:44:04 -07:00
2025-05-05 18:44:20 -07:00
// Open device Settings
2025-05-05 09:33:00 -07:00
Button(action: {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}) {
2025-05-05 10:02:55 -07:00
Image(systemName: "gearshape.fill")
2025-05-05 09:33:00 -07:00
.font(.title2)
2025-05-05 10:02:55 -07:00
.foregroundColor(foregroundColor)
2025-05-05 09:33:00 -07:00
.padding()
}
}
2025-05-05 17:44:04 -07:00
.padding([.leading, .trailing, .bottom], 10)
2025-05-05 09:33:00 -07:00
}
}
2025-05-05 18:44:20 -07:00
// Tap anywhere to toggle dark/light mode
2025-05-05 10:02:55 -07:00
.onTapGesture {
2025-05-05 18:44:20 -07:00
isDarkMode.toggle()
2025-05-05 10:02:55 -07:00
}
2025-05-05 18:44:20 -07:00
// Show alert if permissions or GPS fail
2025-05-05 09:33:00 -07:00
.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)
}
}
)
}
2025-05-05 18:44:20 -07:00
// Update trip distance whenever a new location is received
2025-05-05 09:33:00 -07:00
.onReceive(locationManager.$location) { location in
if let newLocation = location {
if let oldLocation = previousLocation {
let distance = newLocation.distance(from: oldLocation)
tripDistanceMeters += distance
2025-05-05 18:44:20 -07:00
// Save updated trip distance
UserDefaults.standard.set(tripDistanceMeters, forKey: "TripDistance")
2025-05-05 09:33:00 -07:00
}
previousLocation = newLocation
}
}
}
}