diff --git a/Rear Rider/Rear Rider.xcodeproj/project.pbxproj b/Rear Rider/Rear Rider.xcodeproj/project.pbxproj index e9dcc51..2d8adfa 100644 --- a/Rear Rider/Rear Rider.xcodeproj/project.pbxproj +++ b/Rear Rider/Rear Rider.xcodeproj/project.pbxproj @@ -7,8 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 20B1A1BF291C04FC0023B920 /* MetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B1A1BE291C04FC0023B920 /* MetricsView.swift */; }; - 20B1A1C1291C060F0023B920 /* RideHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B1A1C0291C060E0023B920 /* RideHistoryView.swift */; }; 20CEE3372922BE4A00D43613 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 20CEE3362922BE4A00D43613 /* FirebaseFirestore */; }; 20CEE33A2922BE7D00D43613 /* FirestoreModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CEE3392922BE7D00D43613 /* FirestoreModel.swift */; }; 20CEE33C2922BF8D00D43613 /* ViewRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CEE33B2922BF8D00D43613 /* ViewRouter.swift */; }; @@ -18,7 +16,14 @@ 20CEE3472922C03000D43613 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CEE3462922C03000D43613 /* ViewController.swift */; }; 20CEE3492922C37E00D43613 /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 20CEE3482922C37E00D43613 /* FirebaseFirestoreSwift */; }; 20CEE34B2923090A00D43613 /* AuthModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CEE34A2923090A00D43613 /* AuthModel.swift */; }; - 20CEE34D29245C4700D43613 /* MetricsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CEE34C29245C4700D43613 /* MetricsModel.swift */; }; + 20E49B9D293A72BE008EE278 /* ActiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E49B97293A72BE008EE278 /* ActiveView.swift */; }; + 20E49B9E293A72BE008EE278 /* MetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E49B98293A72BE008EE278 /* MetricsView.swift */; }; + 20E49B9F293A72BE008EE278 /* RideHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E49B99293A72BE008EE278 /* RideHistoryView.swift */; }; + 20E49BA1293A72BE008EE278 /* MetricsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E49B9B293A72BE008EE278 /* MetricsModel.swift */; }; + 20E49BA2293A72BE008EE278 /* TrackingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E49B9C293A72BE008EE278 /* TrackingView.swift */; }; + 20E49BA4293A72DE008EE278 /* TrackingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E49BA3293A72DE008EE278 /* TrackingManager.swift */; }; + 20E49BA6293A76E4008EE278 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E49BA5293A76E4008EE278 /* HomeView.swift */; }; + 20E49BA9293DA354008EE278 /* Dates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E49BA8293DA353008EE278 /* Dates.swift */; }; 483E42B4291BE71800B9A3C3 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 483E42B3291BE71800B9A3C3 /* GoogleService-Info.plist */; }; 483E42B7291BE82200B9A3C3 /* FirebaseAnalyticsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 483E42B6291BE82200B9A3C3 /* FirebaseAnalyticsSwift */; }; 483E42B9291BE82200B9A3C3 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 483E42B8291BE82200B9A3C3 /* FirebaseAnalyticsWithoutAdIdSupport */; }; @@ -33,7 +38,6 @@ 4851CE0D28E49485003B5B76 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4851CE0C28E49485003B5B76 /* Assets.xcassets */; }; 4851CE1028E49485003B5B76 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4851CE0F28E49485003B5B76 /* Preview Assets.xcassets */; }; 4851CE1F28E4C3A2003B5B76 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4851CE1E28E4C3A2003B5B76 /* UIImage+Extension.swift */; }; - 487C63AE28E4F8700022F0A7 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 487C63AD28E4F8700022F0A7 /* HomeView.swift */; }; 48CB852D2902E60700283DBD /* RearRiderAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CB852C2902E60700283DBD /* RearRiderAlerts.swift */; }; 48CB852F2902EF5200283DBD /* vehicleAlert.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 48CB852E2902EF5200283DBD /* vehicleAlert.mp3 */; }; 48F4414228F6258C00F7C9AA /* ImageIdentification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48F4414128F6258C00F7C9AA /* ImageIdentification.swift */; }; @@ -63,8 +67,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 20B1A1BE291C04FC0023B920 /* MetricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsView.swift; sourceTree = ""; }; - 20B1A1C0291C060E0023B920 /* RideHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RideHistoryView.swift; sourceTree = ""; }; 20CEE3392922BE7D00D43613 /* FirestoreModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreModel.swift; sourceTree = ""; }; 20CEE33B2922BF8D00D43613 /* ViewRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewRouter.swift; sourceTree = ""; }; 20CEE33F2922BFAF00D43613 /* SignInView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; @@ -72,7 +74,15 @@ 20CEE3412922BFAF00D43613 /* SignUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = ""; }; 20CEE3462922C03000D43613 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 20CEE34A2923090A00D43613 /* AuthModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthModel.swift; sourceTree = ""; }; - 20CEE34C29245C4700D43613 /* MetricsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsModel.swift; sourceTree = ""; }; + 20E49B97293A72BE008EE278 /* ActiveView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveView.swift; sourceTree = ""; }; + 20E49B98293A72BE008EE278 /* MetricsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricsView.swift; sourceTree = ""; }; + 20E49B99293A72BE008EE278 /* RideHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideHistoryView.swift; sourceTree = ""; }; + 20E49B9B293A72BE008EE278 /* MetricsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetricsModel.swift; sourceTree = ""; }; + 20E49B9C293A72BE008EE278 /* TrackingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingView.swift; sourceTree = ""; }; + 20E49BA3293A72DE008EE278 /* TrackingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingManager.swift; sourceTree = ""; }; + 20E49BA5293A76E4008EE278 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 20E49BA7293A77B1008EE278 /* Rear-Rider-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Rear-Rider-Info.plist"; sourceTree = SOURCE_ROOT; }; + 20E49BA8293DA353008EE278 /* Dates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dates.swift; sourceTree = ""; }; 483E42B3291BE71800B9A3C3 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 484558402908941B002D81FC /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = ""; }; 4845584829089EAA002D81FC /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = ""; }; @@ -82,7 +92,6 @@ 4851CE0C28E49485003B5B76 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 4851CE0F28E49485003B5B76 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 4851CE1E28E4C3A2003B5B76 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; - 487C63AD28E4F8700022F0A7 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 48CB852C2902E60700283DBD /* RearRiderAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RearRiderAlerts.swift; sourceTree = ""; }; 48CB852E2902EF5200283DBD /* vehicleAlert.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = vehicleAlert.mp3; sourceTree = ""; }; 48F4414128F6258C00F7C9AA /* ImageIdentification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageIdentification.swift; sourceTree = ""; }; @@ -129,16 +138,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 20B1A1BD291C04E40023B920 /* Metrics */ = { - isa = PBXGroup; - children = ( - 20B1A1BE291C04FC0023B920 /* MetricsView.swift */, - 20B1A1C0291C060E0023B920 /* RideHistoryView.swift */, - 20CEE34C29245C4700D43613 /* MetricsModel.swift */, - ); - path = Metrics; - sourceTree = ""; - }; 20CEE3352922BE4A00D43613 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -165,6 +164,20 @@ path = Auth; sourceTree = ""; }; + 20E49B96293A72BE008EE278 /* Metrics */ = { + isa = PBXGroup; + children = ( + 20E49BA3293A72DE008EE278 /* TrackingManager.swift */, + 20E49B97293A72BE008EE278 /* ActiveView.swift */, + 20E49B98293A72BE008EE278 /* MetricsView.swift */, + 20E49B99293A72BE008EE278 /* RideHistoryView.swift */, + 20E49BA8293DA353008EE278 /* Dates.swift */, + 20E49B9B293A72BE008EE278 /* MetricsModel.swift */, + 20E49B9C293A72BE008EE278 /* TrackingView.swift */, + ); + path = Metrics; + sourceTree = ""; + }; 484558422908942F002D81FC /* StreamingAndRecording */ = { isa = PBXGroup; children = ( @@ -237,12 +250,13 @@ 4851CE0728E49485003B5B76 /* Rear Rider */ = { isa = PBXGroup; children = ( + 20E49BA7293A77B1008EE278 /* Rear-Rider-Info.plist */, + 20E49B96293A72BE008EE278 /* Metrics */, 20CEE33B2922BF8D00D43613 /* ViewRouter.swift */, 20CEE3462922C03000D43613 /* ViewController.swift */, 20CEE33D2922BFAF00D43613 /* Auth */, 20CEE3382922BE6800D43613 /* Firebase */, 483E42B3291BE71800B9A3C3 /* GoogleService-Info.plist */, - 20B1A1BD291C04E40023B920 /* Metrics */, C364D8BD290DAB5900F9EE7C /* Logging */, 4845584729089E87002D81FC /* ConfigurationOptions */, 4845584629089503002D81FC /* ObjectIdentification */, @@ -250,7 +264,7 @@ 4845584329089489002D81FC /* Alerts */, 484558422908942F002D81FC /* StreamingAndRecording */, 4851CE0828E49485003B5B76 /* Rear_RiderApp.swift */, - 487C63AD28E4F8700022F0A7 /* HomeView.swift */, + 20E49BA5293A76E4008EE278 /* HomeView.swift */, C36AA2EB28FC8278001F24A3 /* RiderView.swift */, C3FC5C65290C64B800929D1D /* yolov5m.mlmodel */, 4851CE0C28E49485003B5B76 /* Assets.xcassets */, @@ -400,33 +414,37 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 20E49B9F293A72BE008EE278 /* RideHistoryView.swift in Sources */, 48CB852D2902E60700283DBD /* RearRiderAlerts.swift in Sources */, 4845584B2909768E002D81FC /* ColorExtension.swift in Sources */, 20CEE33C2922BF8D00D43613 /* ViewRouter.swift in Sources */, - 20CEE34D29245C4700D43613 /* MetricsModel.swift in Sources */, 484558412908941B002D81FC /* OptionsView.swift in Sources */, C3BD658328E8DB3B0042CEB4 /* BLEManager.swift in Sources */, 48F4414228F6258C00F7C9AA /* ImageIdentification.swift in Sources */, C307D28C28F2734B001D7F1D /* MJpegStreamingKit.swift in Sources */, 20CEE3442922BFAF00D43613 /* ForgotPasswordView.swift in Sources */, - 20B1A1BF291C04FC0023B920 /* MetricsView.swift in Sources */, 20CEE34B2923090A00D43613 /* AuthModel.swift in Sources */, 20CEE3432922BFAF00D43613 /* SignInView.swift in Sources */, C307D28928F2731F001D7F1D /* CameraTestView.swift in Sources */, C36AA2EC28FC8278001F24A3 /* RiderView.swift in Sources */, + 20E49B9D293A72BE008EE278 /* ActiveView.swift in Sources */, + 20E49BA4293A72DE008EE278 /* TrackingManager.swift in Sources */, C36AA2E728FC80B2001F24A3 /* ShareSheet.swift in Sources */, C3FC5C66290C64B800929D1D /* yolov5m.mlmodel in Sources */, C36AA2E628FC80B2001F24A3 /* RecordingView.swift in Sources */, 20CEE3452922BFAF00D43613 /* SignUpView.swift in Sources */, - 20B1A1C1291C060F0023B920 /* RideHistoryView.swift in Sources */, 4851CE0928E49485003B5B76 /* Rear_RiderApp.swift in Sources */, - 487C63AE28E4F8700022F0A7 /* HomeView.swift in Sources */, 20CEE33A2922BE7D00D43613 /* FirestoreModel.swift in Sources */, C36AA2E828FC80B2001F24A3 /* RecordingExtension.swift in Sources */, 20CEE3472922C03000D43613 /* ViewController.swift in Sources */, + 20E49B9E293A72BE008EE278 /* MetricsView.swift in Sources */, + 20E49BA9293DA354008EE278 /* Dates.swift in Sources */, 4851CE1F28E4C3A2003B5B76 /* UIImage+Extension.swift in Sources */, 4845584929089EAA002D81FC /* UserConfig.swift in Sources */, + 20E49BA6293A76E4008EE278 /* HomeView.swift in Sources */, + 20E49BA2293A72BE008EE278 /* TrackingView.swift in Sources */, C338B28E290F5E94009638F9 /* WifiManager.swift in Sources */, + 20E49BA1293A72BE008EE278 /* MetricsModel.swift in Sources */, C364D8C1290DAFDD00F9EE7C /* RearRiderLogView.swift in Sources */, C364D8BF290DAB9A00F9EE7C /* RearRiderLog.swift in Sources */, ); @@ -577,7 +595,9 @@ DEVELOPMENT_TEAM = 5KVT845VZL; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Rear-Rider-Info.plist"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "This app requires Bluetooth for connecting to RaspberryPi"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Rear Rider requires location to track riding metrics"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -607,7 +627,9 @@ DEVELOPMENT_TEAM = 5KVT845VZL; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Rear-Rider-Info.plist"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "This app requires Bluetooth for connecting to RaspberryPi"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Rear Rider requires location to track riding metrics"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/Rear Rider/Rear Rider/Firebase/FirestoreModel.swift b/Rear Rider/Rear Rider/Firebase/FirestoreModel.swift index 358d77a..99f78e1 100644 --- a/Rear Rider/Rear Rider/Firebase/FirestoreModel.swift +++ b/Rear Rider/Rear Rider/Firebase/FirestoreModel.swift @@ -14,10 +14,11 @@ import FirebaseFirestoreSwift class FirestoreModel: ObservableObject { var db = Firestore.firestore() - var userId = Auth.auth().currentUser?.uid + private var userId = Auth.auth().currentUser?.uid @Published var appError: AppError? = nil @Published var firestoreLoading = false + @Published var rides = [Ride]() struct AppError: Identifiable { let id = UUID().uuidString @@ -33,6 +34,7 @@ class FirestoreModel: ObservableObject { var id = UUID() var res: Result var message: String? + var content: Any? } func writeRide(ride: Ride) -> FirestoreResult { @@ -46,5 +48,29 @@ class FirestoreModel: ObservableObject { return FirestoreResult(res: .failure, message: error.localizedDescription) } } + + func getRides() { + if (rides.isEmpty) { + firestoreLoading = true + } + db.collection("users").document(userId!).collection("rides").order(by: "createdTime", descending: true).addSnapshotListener() { (querySnapshot, err) in + if let err = err { + self.firestoreLoading = false + print("Error getting documents: \(err)") + } else { + self.rides.removeAll() + for document in querySnapshot!.documents { + do { + let docData = try document.data(as: Ride.self) + self.rides.append(docData) + } catch { + print(error) + self.firestoreLoading = false + } + } + } + self.firestoreLoading = false + } + } } diff --git a/Rear Rider/Rear Rider/HomeView.swift b/Rear Rider/Rear Rider/HomeView.swift index 0c007da..5052c17 100644 --- a/Rear Rider/Rear Rider/HomeView.swift +++ b/Rear Rider/Rear Rider/HomeView.swift @@ -33,14 +33,19 @@ struct HomeView: View { .tabItem { Image(systemName: "camera") } - OptionsView() + ActiveView() .tabItem { - Image(systemName: "gear") - } + Image(systemName: "location") + } + RideHistoryView() .tabItem { Image(systemName: "chart.bar.xaxis") } + OptionsView() + .tabItem { + Image(systemName: "gear") + } } } } diff --git a/Rear Rider/Rear Rider/Metrics/ActiveView.swift b/Rear Rider/Rear Rider/Metrics/ActiveView.swift new file mode 100644 index 0000000..3942931 --- /dev/null +++ b/Rear Rider/Rear Rider/Metrics/ActiveView.swift @@ -0,0 +1,77 @@ +// +// ActiveView.swift +// Rear Rider +// +// Created by Paul Sutton on 11/30/22. +// + +import SwiftUI + +struct ActiveView: View { + @StateObject var trackingManager = TrackingManager() + var body: some View { + NavigationView { + switch trackingManager.authorizationStatus { + case .notDetermined: + AnyView(RequestLocationView()) + .environmentObject(trackingManager) + case .restricted: + ErrorView(errorText: "Location use is restricted. Please enable location use while app is in use in settings") + case .denied: + ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.") + case .authorizedAlways, .authorizedWhenInUse: + TrackingView().environmentObject(trackingManager) + default: + Text("Unexpected status") + } + } + } +} + +struct RequestLocationView: View { + @EnvironmentObject var trackingManager: TrackingManager + + var body: some View { + VStack { + Image(systemName: "location.circle") + .resizable() + .frame(width: 100, height: 100, alignment: .center) + .foregroundColor(/*@START_MENU_TOKEN@*/.blue/*@END_MENU_TOKEN@*/) + Button(action: { + trackingManager.requestPermission() + }, label: { + Label("Allow tracking", systemImage: "location") + }) + .padding(10) + .foregroundColor(.white) + .background(Color.blue) + .clipShape(RoundedRectangle(cornerRadius: 8)) + Text("We need your permission to track you.") + .foregroundColor(.gray) + .font(.caption) + } + } +} + +struct ErrorView: View { + var errorText: String + + var body: some View { + VStack { + Image(systemName: "xmark.octagon") + .resizable() + .frame(width: 100, height: 100, alignment: .center) + Text(errorText) + } + .padding() + .foregroundColor(.white) + .background(Color.red) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } +} + +struct ActiveView_Previews: PreviewProvider { + static var previews: some View { + ActiveView() + } +} diff --git a/Rear Rider/Rear Rider/Metrics/Dates.swift b/Rear Rider/Rear Rider/Metrics/Dates.swift new file mode 100644 index 0000000..7613442 --- /dev/null +++ b/Rear Rider/Rear Rider/Metrics/Dates.swift @@ -0,0 +1,27 @@ +// +// Dates.swift +// timerr +// +// Created by Paul Sutton on 5/9/22. +// + +import Foundation +import FirebaseFirestore +// date formatter singleton +class Formatter { + static let shared = Formatter(dateFormatter: DateFormatter()) + let dateFormatter: DateFormatter + private init(dateFormatter: DateFormatter) { + self.dateFormatter = dateFormatter + dateFormatter.dateFormat = "MM/dd/yy h:mm a" + } + func timestampToString(timestamp: Timestamp?) -> String { + if ((timestamp) != nil) { + let unformattedDate = timestamp!.dateValue() + let formattedDateString = dateFormatter.string(from: unformattedDate) + return formattedDateString + } else { + return "" + } + } +} diff --git a/Rear Rider/Rear Rider/Metrics/MetricsModel.swift b/Rear Rider/Rear Rider/Metrics/MetricsModel.swift index 1f9299f..702503b 100644 --- a/Rear Rider/Rear Rider/Metrics/MetricsModel.swift +++ b/Rear Rider/Rear Rider/Metrics/MetricsModel.swift @@ -12,13 +12,20 @@ import FirebaseFirestoreSwift struct Split: Hashable, Codable, Identifiable { var id = UUID() var seconds: Int - var distance: Int + var distance: Double +} + +struct FormattedSplit: Hashable, Codable, Identifiable { + var id = UUID() + var milestone: Int + var speed: Int } struct Ride: Hashable, Codable, Identifiable { @DocumentID var id: String? @ServerTimestamp var createdTime: Timestamp? - var totalDistance: Int + var totalDistance: Double + var totalSeconds: Int var metric: String - var creatorId: String + var splits: Array } diff --git a/Rear Rider/Rear Rider/Metrics/MetricsView.swift b/Rear Rider/Rear Rider/Metrics/MetricsView.swift index da6e200..1e4439c 100644 --- a/Rear Rider/Rear Rider/Metrics/MetricsView.swift +++ b/Rear Rider/Rear Rider/Metrics/MetricsView.swift @@ -9,6 +9,10 @@ import SwiftUI import Charts struct MetricsView: View { + @State var cumDist = 0 + @State var formattedSplits = [FormattedSplit]() + @State var loading = true + var ride: Ride var body: some View { List { Section("Summary Stats") { @@ -16,87 +20,81 @@ struct MetricsView: View { HStack { Text("Total Distance").bold() Spacer() - Text("5.8 miles") + let dist = metersToMiles(meters: ride.totalDistance) + Text("\(metersToMiles(meters: ride.totalDistance), specifier: "%.2f") miles") } Spacer() HStack { Text("Total Time").bold() Spacer() - Text("23 minutes") + let (h,m,s) = secondsToHoursMinutesSeconds(ride.totalSeconds) + let formattedTime = String(format: "%02d:%02d:%02d", h, m, s) + Text(formattedTime) } Spacer() HStack { Text("Average Pace").bold() Spacer() - Text("12.4 mph") + let avgMps = ride.totalDistance / Double(ride.totalSeconds) + let avgMph = avgMps * 2.237 + + Text("\(avgMph, specifier: "%.2f") mph") } - }.padding() - }.headerProminence(.increased) - Section("Splits") { - HStack { - Text("Mile 1") - Spacer() - Text("4 min 03 sec") - } - HStack { - Text("Mile 2") - Spacer() - Text("4 min 20 sec") - } - HStack { - Text("Mile 3") - Spacer() - Text("3 min 49 sec") - } - HStack { - Text("Mile 4") - Spacer() - Text("4 min 15 sec") - } - HStack { - Text("Mile 5") Spacer() - Text("4 min 10 sec") - } + HStack { + Text("Calories Burned").bold() + Spacer() + let cals = ride.totalDistance / 1000 * 32 + Text("\(cals, specifier: "%.2f") cals") + } + }.padding() }.headerProminence(.increased) - Section("Pace") { - if #available(iOS 16.0, *) { - Chart { - LineMark( - x: .value("Distance", "1 mi"), - y: .value("Value", 16) - ) - LineMark( - x: .value("Distance", "2 mi"), - y: .value("Value", 14) - ) - LineMark( - x: .value("Distance", "3 mi"), - y: .value("Value", 17.5) - ) - LineMark( - x: .value("Distance", "4 mi"), - y: .value("Value", 15) - ) - - LineMark( - x: .value("Distance", "5 mi"), - y: .value("Value", 15.3) - ) - - }.chartXAxisLabel("Distance (mi)").chartYAxisLabel("Pace (mph)") - .frame(height: 250) - } else { - // Fallback on earlier versions - Text("IOS 16 required to display chart") + if (loading) { + ProgressView("Loading") + } else { + if (!ride.splits.isEmpty) { + Section("Splits") { + ForEach(ride.splits.indices, id: \.self) {index in + HStack { + Text("Mile \(index + 1)") + Spacer() + let (splH,splM,splS) = secondsToHoursMinutesSeconds(ride.splits[index].seconds) + let formattedSplitTime = String(format: "%02d:%02d", splM, splS) + Text(formattedSplitTime) + } + } + }.headerProminence(.increased) + Section("Pace") { + if #available(iOS 16.0, *) { + Chart { + ForEach(formattedSplits) { split in + BarMark( + x: .value("Milestone", split.milestone), + y: .value("Speed", split.speed) + ) + + } + }.chartXAxisLabel("Distance (miles)").chartYAxisLabel("Pace (mph)") + .frame(height: 250) + } else { + // Fallback on earlier versions + Text("IOS 16 required to display chart") + } + }.headerProminence(.increased) } - }.headerProminence(.increased) - }.navigationTitle("Metrics") + } + }.navigationTitle("Metrics").onAppear { + for split in ride.splits { + let speedMps = split.distance / Double(split.seconds) + let speedMph = speedMps * 2.237 + let splitMiles = split.distance / 1609 + cumDist += Int(splitMiles) + formattedSplits.append(FormattedSplit(milestone: cumDist, speed: Int(speedMph))) + } + loading = false + } } -} - -struct MetricsView_Previews: PreviewProvider { - static var previews: some View { - MetricsView() + func secondsToHoursMinutesSeconds(_ seconds: Int) -> (Int, Int, Int) { + return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60) } } diff --git a/Rear Rider/Rear Rider/Metrics/RideHistoryView.swift b/Rear Rider/Rear Rider/Metrics/RideHistoryView.swift index 9bac637..f626b0c 100644 --- a/Rear Rider/Rear Rider/Metrics/RideHistoryView.swift +++ b/Rear Rider/Rear Rider/Metrics/RideHistoryView.swift @@ -8,45 +8,26 @@ import SwiftUI struct RideHistoryView: View { + @EnvironmentObject var firestoreModel: FirestoreModel var body: some View { NavigationView { List { - NavigationLink(destination: MetricsView()) { - HStack { - Text("11/9/2022").bold() - Spacer() - Text("5 miles").bold().foregroundColor(.gray) + if (firestoreModel.firestoreLoading) { + ProgressView() + } else { + ForEach(firestoreModel.rides.indices, id: \.self) {index in + NavigationLink(destination: MetricsView(ride: firestoreModel.rides[index])) { + HStack { + Text(Formatter.shared.timestampToString(timestamp: firestoreModel.rides[index].createdTime)).bold() + Spacer() + Text("\(metersToMiles(meters: firestoreModel.rides[index].totalDistance), specifier: "%.2f") miles").bold().foregroundColor(.gray) + } + } } } - NavigationLink(destination: MetricsView()) { - HStack { - Text("11/9/2022").bold() - Spacer() - Text("5.8 miles").bold().foregroundColor(.gray) - } - } - NavigationLink(destination: MetricsView()) { - HStack { - Text("11/10/2022").bold() - Spacer() - Text("6 miles").bold().foregroundColor(.gray) - } - } - NavigationLink(destination: MetricsView()) { - HStack { - Text("11/11/2022").bold() - Spacer() - Text("9 miles").bold().foregroundColor(.gray) - } - } - NavigationLink(destination: MetricsView()) { - HStack { - Text("11/12/2022").bold() - Spacer() - Text("3.3 miles").bold().foregroundColor(.gray) - } - } - }.navigationTitle("My Rides") + }.navigationTitle("My Rides").onAppear { + firestoreModel.getRides() + } } } } diff --git a/Rear Rider/Rear Rider/Metrics/TrackingManager.swift b/Rear Rider/Rear Rider/Metrics/TrackingManager.swift new file mode 100644 index 0000000..36d8c38 --- /dev/null +++ b/Rear Rider/Rear Rider/Metrics/TrackingManager.swift @@ -0,0 +1,116 @@ +// +// LocationManager.swift +// Rear Rider +// +// Created by Paul Sutton on 11/30/22. +// + +import Foundation +import CoreLocation + +class TrackingManager: NSObject, ObservableObject, CLLocationManagerDelegate { + @Published var authorizationStatus: CLAuthorizationStatus + @Published var lastSeenLocation: CLLocation? + @Published var mode: TrackingMode = .stopped + @Published var locationsArray = [CLLocation]() + @Published var cummulativeDistance = 0.0 + @Published var hours = 0 + @Published var minutes = 0 + @Published var seconds = 0 + @Published var secondsElapsed = 0 + private var milestone = 0.0 + private var secondsMilestone = 0 + @Published var splits = [Split]() + private var splitDistance = 1609 + + private var timer = Timer() + + private let locationManager: CLLocationManager + + override init() { + locationManager = CLLocationManager() + authorizationStatus = locationManager.authorizationStatus + + super.init() + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation + locationManager.distanceFilter = kCLDistanceFilterNone + locationManager.allowsBackgroundLocationUpdates = true + locationManager.showsBackgroundLocationIndicator = true + } + + + enum TrackingMode { + case started + case stopped + case paused + } + + func requestPermission() { + locationManager.requestWhenInUseAuthorization() + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + authorizationStatus = manager.authorizationStatus + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if (locationsArray.isEmpty) { + locationsArray.append(locations.first!) + lastSeenLocation = locations.first + guard let dist = locations.first?.distance(from: locationsArray.last!) else{return} + cummulativeDistance += dist + } else { + lastSeenLocation = locations.first + print("speed: \(lastSeenLocation?.speed ?? -7)") + print("accuracy: \(lastSeenLocation?.speedAccuracy ?? -7)") + guard let dist = locations.first?.distance(from: locationsArray.last!) else{return} + locationsArray.append(locations.first!) + cummulativeDistance += dist + if (cummulativeDistance - milestone >= Double(splitDistance)) { + splits.append(Split(seconds: secondsMilestone, distance: Double(splitDistance))) + milestone = cummulativeDistance + secondsMilestone = 0 + } + } + } + + func start() { + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in + self.secondsElapsed = self.secondsElapsed + 1 + self.secondsMilestone = self.secondsMilestone + 1 + let (h,m,s) = self.secondsToHoursMinutesSeconds(self.secondsElapsed) + self.hours = h + self.minutes = m + self.seconds = s + } + locationManager.startUpdatingLocation() + mode = .started + } + + func pause() { + locationManager.stopUpdatingLocation() + timer.invalidate() + mode = .paused + } + + func stop() { + locationManager.stopUpdatingLocation() + timer.invalidate() + secondsElapsed = 0 + seconds = 0 + minutes = 0 + hours = 0 + cummulativeDistance = 0.0 + milestone = 0.0 + secondsMilestone = 0 + locationsArray.removeAll() + splits.removeAll() + lastSeenLocation = nil + mode = .stopped + } + + func secondsToHoursMinutesSeconds(_ seconds: Int) -> (Int, Int, Int) { + return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60) + } +} diff --git a/Rear Rider/Rear Rider/Metrics/TrackingView.swift b/Rear Rider/Rear Rider/Metrics/TrackingView.swift new file mode 100644 index 0000000..710f983 --- /dev/null +++ b/Rear Rider/Rear Rider/Metrics/TrackingView.swift @@ -0,0 +1,114 @@ +// +// TrackingView.sÏwift +// Rear Rider +// +// Created by Paul Sutton on 11/30/22. +// + +import SwiftUI + +struct TrackingView: View { + @EnvironmentObject var trackingManager: TrackingManager + @EnvironmentObject var db: FirestoreModel + @State private var loading = false + @State private var err = false + var body: some View { + VStack { + HStack{ + VStack(alignment: .leading){ + Text("Elapsed Time").bold().foregroundColor(.gray) + let formattedTime = String(format: "%02d:%02d:%02d", trackingManager.hours, trackingManager.minutes, trackingManager.seconds) + Text(formattedTime).font(.system(size: 50)).fontWeight(.bold).monospacedDigit().padding(.bottom, 20) + Text("Distance").bold().foregroundColor(.gray) + Text("\(metersToMiles(meters: trackingManager.cummulativeDistance), specifier: "%.2f") miles").font(.system(size: 50)).fontWeight(.bold).monospacedDigit().padding(.bottom, 20) + if (db.firestoreLoading) { + ProgressView("Writing ride to db") + } + } + Spacer() + }.padding(25) + + Spacer() + VStack{ + if trackingManager.mode == .stopped { + Button(action: { + self.trackingManager.start() + }) { + Text("Start") + .bold() + .frame(width: 275, height: 50) + .background(.green) + .foregroundColor(.white) + .cornerRadius(10) + } + } + if trackingManager.mode == .started { + Button(action: { + self.trackingManager.pause() + }) { + Text("Pause") + .bold() + .frame(width: 150, height: 50) + .background(.red) + .foregroundColor(.white) + .cornerRadius(10) + } + } + if trackingManager.mode == .paused { + HStack { + Spacer() + Button(action: { + self.trackingManager.start() + }) { + Text("Resume") + .bold() + .frame(width: 150, height: 50) + .background(.green) + .foregroundColor(.white) + .cornerRadius(10) + } + Spacer() + Button(action: { + createRun() + + }) { + Text("End Ride") + .bold() + .frame(width: 150, height: 50) + .background(.red) + .foregroundColor(.white) + .cornerRadius(10) + } + Spacer() + } + } + }.padding(.bottom, 40) + }.frame(maxHeight: .infinity, alignment: .bottom).alert("Error writing ride to DB", isPresented: $err) { + Button("OK", role: .cancel) { } + } + } + + func createRun() { + let result = db.writeRide(ride: Ride(totalDistance: trackingManager.cummulativeDistance, totalSeconds: trackingManager.secondsElapsed, metric: "meters", splits: trackingManager.splits)) + if (result.res == .success) { + self.trackingManager.stop() + } + if (result.res == .failure) { + self.trackingManager.stop() + err = true + } + } +} + + + +func metersToMiles(meters: Double) -> Double { + return meters/1609 +} + + +struct TrackingView_Previews: PreviewProvider { + static var previews: some View { + TrackingView() + } +}