From 3001b8d2f8a61377a180e3736377d45e9c74117d Mon Sep 17 00:00:00 2001 From: Aayush Jain <118139337+aajain2@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:50:57 -0800 Subject: [PATCH 1/5] firebase integration (coughevents documents) --- CoughSync/CoughDetection/CoughClass.swift | 8 ++- .../CoughDetectionViewModel.swift | 24 ++++++++- CoughSync/CoughSyncStandard.swift | 31 ++++++++++++ CoughSync/CoughView/CoughModelView.swift | 50 ++++++++++++------- CoughSync/Dashboard/Dashboard.swift | 38 +++++++++----- CoughSync/Resources/Localizable.xcstrings | 3 ++ 6 files changed, 119 insertions(+), 35 deletions(-) diff --git a/CoughSync/CoughDetection/CoughClass.swift b/CoughSync/CoughDetection/CoughClass.swift index a6143e3..9daab35 100644 --- a/CoughSync/CoughDetection/CoughClass.swift +++ b/CoughSync/CoughDetection/CoughClass.swift @@ -13,11 +13,15 @@ import Foundation -class Cough { +class Cough: Identifiable, Codable { let timestamp: Date + let id: UUID + let confidence: Double - init(timestamp: Date = Date()) { + init(timestamp: Date = Date(), id: UUID = UUID(), confidence: Double = 0) { self.timestamp = timestamp + self.id = id + self.confidence = confidence } } diff --git a/CoughSync/CoughDetection/CoughDetectionViewModel.swift b/CoughSync/CoughDetection/CoughDetectionViewModel.swift index 4433d7f..bb07fd1 100644 --- a/CoughSync/CoughDetection/CoughDetectionViewModel.swift +++ b/CoughSync/CoughDetection/CoughDetectionViewModel.swift @@ -17,12 +17,17 @@ import Combine import Foundation import Observation import SoundAnalysis +import Spezi +import SwiftUI @Observable @MainActor class CoughDetectionViewModel { @ObservationIgnored let coughAnalysisManager = CoughAnalysisManager.shared + // Environment property to access the standard + private let standard: CoughSyncStandard + @ObservationIgnored var lastTime: Double = 0 var detectionStarted = false @@ -34,6 +39,11 @@ class CoughDetectionViewModel { var identifiedSound: (identifier: String, confidence: String)? private var detectionCancellable: AnyCancellable? + // Initialize with standard from environment + init(standard: CoughSyncStandard) { + self.standard = standard + } + private func formattedDetectionResult(_ result: SNClassificationResult) -> (identifier: String, confidence: String)? { guard let classification = result.classifications.first else { return nil } @@ -50,9 +60,14 @@ class CoughDetectionViewModel { let confidencePercentString = String(format: "%.2f%%", confidence * 100.0) print("\(displayName): \(confidencePercentString) confidence.\n") - if displayName == "Coughs" { - let cough = Cough(timestamp: Date()) + if displayName == "Coughs" && confidence > 0.5 { + let cough = Cough(timestamp: Date(), confidence: confidence) coughCollection.addCough(cough) + + // Store the cough in Firebase + Task { + await standard.add(cough: cough) + } } return (displayName, confidencePercentString) @@ -80,4 +95,9 @@ class CoughDetectionViewModel { identifiedSound = nil coughAnalysisManager.stopCoughDetection() } + + func syncCoughsToFirebase() async { + // This could be called when app goes to background or when stopping detection + // Implementation depends on how you want to track unsaved coughs + } } diff --git a/CoughSync/CoughSyncStandard.swift b/CoughSync/CoughSyncStandard.swift index 167f3e9..9247ea8 100644 --- a/CoughSync/CoughSyncStandard.swift +++ b/CoughSync/CoughSyncStandard.swift @@ -133,4 +133,35 @@ actor CoughSyncStandard: Standard, await logger.error("Could not store consent form: \(error)") } } + + func add(cough: Cough) async { + if FeatureFlags.disableFirebase { + logger.debug("Received new cough event: \(cough.timestamp)") + return + } + + do { + try await configuration.userDocumentReference + .collection("CoughEvents") // Store all cough events in a /CoughEvents collection + .document(UUID().uuidString) // Generate a unique ID for each cough event + .setData([ + "timestamp": cough.timestamp, + "confidence": cough.confidence + ]) + } catch { + logger.error("Could not store cough event: \(error)") + } + } + + // For batch saving multiple coughs at once (optional) + func add(coughs: [Cough]) async { + if FeatureFlags.disableFirebase { + logger.debug("Received batch of \(coughs.count) cough events") + return + } + + for cough in coughs { + await add(cough: cough) + } + } } diff --git a/CoughSync/CoughView/CoughModelView.swift b/CoughSync/CoughView/CoughModelView.swift index 04421db..0ce249f 100644 --- a/CoughSync/CoughView/CoughModelView.swift +++ b/CoughSync/CoughView/CoughModelView.swift @@ -13,51 +13,63 @@ // import SwiftUI +import Spezi + struct CoughModelView: View { - @State var viewModel = CoughDetectionViewModel() + @Environment(CoughSyncStandard.self) private var standard + @State private var viewModel: CoughDetectionViewModel? var body: some View { VStack { - Spacer() - detectionStatusView() - Spacer() - microphoneButton() + if let viewModel = viewModel { + Spacer() + detectionStatusView() + Spacer() + microphoneButton() + .padding() + } else { + // Show a loading indicator + ProgressView("Loading...") + } + } + .onAppear { + // Initialize viewModel here when environment is available + viewModel = CoughDetectionViewModel(standard: standard) } - .padding() } private var microphoneImage: some View { - Image(systemName: viewModel.detectionStarted ? "stop.fill" : "mic.fill") + Image(systemName: viewModel?.detectionStarted == true ? "stop.fill" : "mic.fill") .font(.system(size: 50)) .padding(30) - .background(viewModel.detectionStarted ? .gray.opacity(0.7) : .blue) + .background(viewModel?.detectionStarted == true ? .gray.opacity(0.7) : .blue) .foregroundStyle(.white) .clipShape(Circle()) .shadow(color: .gray, radius: 5) .contentTransition(.symbolEffect(.replace)) - .accessibilityLabel(viewModel.detectionStarted ? "Stop sound detection" : "Start sound detection") + .accessibilityLabel(viewModel?.detectionStarted == true ? "Stop sound detection" : "Start sound detection") } @ViewBuilder private func detectionStatusView() -> some View { - if !viewModel.detectionStarted { + if viewModel?.detectionStarted == false { VStack(spacing: 10) { ContentUnavailableView( "No Sound Detected", systemImage: "waveform.badge.magnifyingglass", description: Text("Tap the microphone to start detecting") ) - Text("Cough Count: \(viewModel.coughCount)") + Text("Cough Count: \(viewModel?.coughCount ?? 0)") .font(.title2) .foregroundColor(.secondary) } - } else if let predictedSound = viewModel.identifiedSound { + } else if let predictedSound = viewModel?.identifiedSound { VStack(spacing: 10) { Text(predictedSound.0) .font(.system(size: 26)) - Text("Cough Count: \(viewModel.coughCount)") - Text("Coughs Today: \(viewModel.coughCollection.coughsToday())") - Text("Cough Difference: \(viewModel.coughCollection.coughDiffDay())") + Text("Cough Count: \(viewModel?.coughCount ?? 0)") + Text("Coughs Today: \(viewModel?.coughCollection.coughsToday() ?? 0)") + Text("Cough Difference: \(viewModel?.coughCollection.coughDiffDay() ?? 0)") } .multilineTextAlignment(.center) .padding() @@ -78,12 +90,12 @@ struct CoughModelView: View { private func toggleListening() { withAnimation { - viewModel.detectionStarted.toggle() + viewModel?.detectionStarted.toggle() } - if viewModel.detectionStarted { - viewModel.startListening() + if viewModel?.detectionStarted == true { + viewModel?.startListening() } else { - viewModel.stopListening() + viewModel?.stopListening() } } } diff --git a/CoughSync/Dashboard/Dashboard.swift b/CoughSync/Dashboard/Dashboard.swift index 75ff914..be2a87b 100644 --- a/CoughSync/Dashboard/Dashboard.swift +++ b/CoughSync/Dashboard/Dashboard.swift @@ -20,23 +20,33 @@ import SpeziSchedulerUI import SpeziViews import SwiftUI - struct Dashboard: View { @Environment(Account.self) private var account: Account? + @Environment(CoughSyncStandard.self) private var standard @Binding var presentingAccount: Bool - @State private var viewModel = CoughDetectionViewModel() + // Don't initialize viewModel right away + @State private var viewModel: CoughDetectionViewModel? @State private var previousCoughCount: Int = 0 + init(presentingAccount: Binding) { + self._presentingAccount = presentingAccount + } + var body: some View { NavigationStack { ScrollView { - VStack(spacing: 20) { - coughSummaryCard() - coughStats() - Divider() + if let viewModel = viewModel { + VStack(spacing: 20) { + coughSummaryCard() + coughStats() + Divider() + } + .padding() + } else { + // Show a loading indicator or placeholder + ProgressView("Loading...") } - .padding() } .navigationTitle("Summary") .toolbar { @@ -45,10 +55,14 @@ struct Dashboard: View { } } .onAppear { - previousCoughCount = viewModel.coughCount + // Initialize viewModel here when environment is available + viewModel = CoughDetectionViewModel(standard: standard) + } + .onAppear { + previousCoughCount = viewModel?.coughCount ?? 0 } - .onChange(of: viewModel.coughCount) { oldValue, _ in - previousCoughCount = oldValue + .onChange(of: viewModel?.coughCount) { oldValue, _ in + previousCoughCount = oldValue ?? 0 } } } @@ -61,7 +75,7 @@ struct Dashboard: View { Text("Today") .font(.headline) .foregroundColor(.primary) - Text("\(viewModel.coughCount) ") + Text("\(viewModel?.coughCount ?? 0) ") .font(.largeTitle) .bold() .foregroundColor(.blue) @@ -112,7 +126,7 @@ struct Dashboard: View { @ViewBuilder private func statusCircle() -> some View { - let change = viewModel.coughCount - previousCoughCount + let change = viewModel?.coughCount ?? 0 - previousCoughCount let color: Color = change > 0 ? .red : (change < 0 ? .green : .blue) let trendSymbol = change > 0 ? "↑" : (change < 0 ? "↓" : "–") diff --git a/CoughSync/Resources/Localizable.xcstrings b/CoughSync/Resources/Localizable.xcstrings index 860da04..65912ac 100644 --- a/CoughSync/Resources/Localizable.xcstrings +++ b/CoughSync/Resources/Localizable.xcstrings @@ -337,6 +337,9 @@ } } } + }, + "Loading..." : { + }, "Next" : { "localizations" : { From 0d9808dd79807e9dffe1d1acac533bdf034e6866 Mon Sep 17 00:00:00 2001 From: Aayush Jain <118139337+aajain2@users.noreply.github.com> Date: Wed, 5 Mar 2025 09:06:40 -0800 Subject: [PATCH 2/5] PR fix errors / style errors --- .../CoughDetection/CoughDetectionViewModel.swift | 5 ----- CoughSync/CoughSyncStandard.swift | 12 ------------ CoughSync/CoughView/CoughModelView.swift | 2 +- CoughSync/Dashboard/Dashboard.swift | 2 -- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/CoughSync/CoughDetection/CoughDetectionViewModel.swift b/CoughSync/CoughDetection/CoughDetectionViewModel.swift index bb07fd1..823f892 100644 --- a/CoughSync/CoughDetection/CoughDetectionViewModel.swift +++ b/CoughSync/CoughDetection/CoughDetectionViewModel.swift @@ -95,9 +95,4 @@ class CoughDetectionViewModel { identifiedSound = nil coughAnalysisManager.stopCoughDetection() } - - func syncCoughsToFirebase() async { - // This could be called when app goes to background or when stopping detection - // Implementation depends on how you want to track unsaved coughs - } } diff --git a/CoughSync/CoughSyncStandard.swift b/CoughSync/CoughSyncStandard.swift index 9247ea8..cd75ae4 100644 --- a/CoughSync/CoughSyncStandard.swift +++ b/CoughSync/CoughSyncStandard.swift @@ -152,16 +152,4 @@ actor CoughSyncStandard: Standard, logger.error("Could not store cough event: \(error)") } } - - // For batch saving multiple coughs at once (optional) - func add(coughs: [Cough]) async { - if FeatureFlags.disableFirebase { - logger.debug("Received batch of \(coughs.count) cough events") - return - } - - for cough in coughs { - await add(cough: cough) - } - } } diff --git a/CoughSync/CoughView/CoughModelView.swift b/CoughSync/CoughView/CoughModelView.swift index 0ce249f..b516c60 100644 --- a/CoughSync/CoughView/CoughModelView.swift +++ b/CoughSync/CoughView/CoughModelView.swift @@ -12,8 +12,8 @@ // Created by Ethan Bell on 12/2/2025. // -import SwiftUI import Spezi +import SwiftUI struct CoughModelView: View { @Environment(CoughSyncStandard.self) private var standard diff --git a/CoughSync/Dashboard/Dashboard.swift b/CoughSync/Dashboard/Dashboard.swift index be2a87b..c3d6d29 100644 --- a/CoughSync/Dashboard/Dashboard.swift +++ b/CoughSync/Dashboard/Dashboard.swift @@ -24,8 +24,6 @@ struct Dashboard: View { @Environment(Account.self) private var account: Account? @Environment(CoughSyncStandard.self) private var standard @Binding var presentingAccount: Bool - - // Don't initialize viewModel right away @State private var viewModel: CoughDetectionViewModel? @State private var previousCoughCount: Int = 0 From a6efbe8fc3bc405251389e5597eeda576e9132ee Mon Sep 17 00:00:00 2001 From: Aayush Jain <118139337+aajain2@users.noreply.github.com> Date: Wed, 5 Mar 2025 12:09:03 -0800 Subject: [PATCH 3/5] type order fix --- CoughSync/CoughView/CoughModelView.swift | 2 +- CoughSync/Dashboard/Dashboard.swift | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CoughSync/CoughView/CoughModelView.swift b/CoughSync/CoughView/CoughModelView.swift index b516c60..0ce249f 100644 --- a/CoughSync/CoughView/CoughModelView.swift +++ b/CoughSync/CoughView/CoughModelView.swift @@ -12,8 +12,8 @@ // Created by Ethan Bell on 12/2/2025. // -import Spezi import SwiftUI +import Spezi struct CoughModelView: View { @Environment(CoughSyncStandard.self) private var standard diff --git a/CoughSync/Dashboard/Dashboard.swift b/CoughSync/Dashboard/Dashboard.swift index c3d6d29..07aa295 100644 --- a/CoughSync/Dashboard/Dashboard.swift +++ b/CoughSync/Dashboard/Dashboard.swift @@ -21,16 +21,16 @@ import SpeziViews import SwiftUI struct Dashboard: View { + // MARK: - Type Properties + + // MARK: - Instance Properties @Environment(Account.self) private var account: Account? @Environment(CoughSyncStandard.self) private var standard @Binding var presentingAccount: Bool @State private var viewModel: CoughDetectionViewModel? @State private var previousCoughCount: Int = 0 - init(presentingAccount: Binding) { - self._presentingAccount = presentingAccount - } - + // MARK: - Computed Properties var body: some View { NavigationStack { ScrollView { @@ -65,6 +65,12 @@ struct Dashboard: View { } } + // MARK: - Initializers + init(presentingAccount: Binding) { + self._presentingAccount = presentingAccount + } + + // MARK: - Methods @ViewBuilder private func coughSummaryCard() -> some View { VStack { From 94b373f56d2883548fad06a7f9ad1d2215e89e55 Mon Sep 17 00:00:00 2001 From: Aayush Jain <118139337+aajain2@users.noreply.github.com> Date: Wed, 5 Mar 2025 12:18:34 -0800 Subject: [PATCH 4/5] order fix --- CoughSync/Dashboard/Dashboard.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/CoughSync/Dashboard/Dashboard.swift b/CoughSync/Dashboard/Dashboard.swift index 07aa295..d2e382d 100644 --- a/CoughSync/Dashboard/Dashboard.swift +++ b/CoughSync/Dashboard/Dashboard.swift @@ -21,8 +21,6 @@ import SpeziViews import SwiftUI struct Dashboard: View { - // MARK: - Type Properties - // MARK: - Instance Properties @Environment(Account.self) private var account: Account? @Environment(CoughSyncStandard.self) private var standard @@ -30,7 +28,12 @@ struct Dashboard: View { @State private var viewModel: CoughDetectionViewModel? @State private var previousCoughCount: Int = 0 - // MARK: - Computed Properties + // MARK: - Initializers + init(presentingAccount: Binding) { + self._presentingAccount = presentingAccount + } + + // MARK: - Body var body: some View { NavigationStack { ScrollView { @@ -65,11 +68,6 @@ struct Dashboard: View { } } - // MARK: - Initializers - init(presentingAccount: Binding) { - self._presentingAccount = presentingAccount - } - // MARK: - Methods @ViewBuilder private func coughSummaryCard() -> some View { From c824af3d349c195adecddb964f342c43e406434b Mon Sep 17 00:00:00 2001 From: Aayush Jain <118139337+aajain2@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:08:15 -0800 Subject: [PATCH 5/5] style fixes --- CoughSync/CoughView/CoughModelView.swift | 2 +- CoughSync/Dashboard/Dashboard.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CoughSync/CoughView/CoughModelView.swift b/CoughSync/CoughView/CoughModelView.swift index 0ce249f..b516c60 100644 --- a/CoughSync/CoughView/CoughModelView.swift +++ b/CoughSync/CoughView/CoughModelView.swift @@ -12,8 +12,8 @@ // Created by Ethan Bell on 12/2/2025. // -import SwiftUI import Spezi +import SwiftUI struct CoughModelView: View { @Environment(CoughSyncStandard.self) private var standard diff --git a/CoughSync/Dashboard/Dashboard.swift b/CoughSync/Dashboard/Dashboard.swift index d2e382d..40e6f7f 100644 --- a/CoughSync/Dashboard/Dashboard.swift +++ b/CoughSync/Dashboard/Dashboard.swift @@ -28,11 +28,6 @@ struct Dashboard: View { @State private var viewModel: CoughDetectionViewModel? @State private var previousCoughCount: Int = 0 - // MARK: - Initializers - init(presentingAccount: Binding) { - self._presentingAccount = presentingAccount - } - // MARK: - Body var body: some View { NavigationStack { @@ -68,6 +63,11 @@ struct Dashboard: View { } } + // MARK: - Initializers + init(presentingAccount: Binding) { + self._presentingAccount = presentingAccount + } + // MARK: - Methods @ViewBuilder private func coughSummaryCard() -> some View {