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..823f892 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) diff --git a/CoughSync/CoughSyncStandard.swift b/CoughSync/CoughSyncStandard.swift index 167f3e9..cd75ae4 100644 --- a/CoughSync/CoughSyncStandard.swift +++ b/CoughSync/CoughSyncStandard.swift @@ -133,4 +133,23 @@ 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)") + } + } } diff --git a/CoughSync/CoughView/CoughModelView.swift b/CoughSync/CoughView/CoughModelView.swift index 04421db..b516c60 100644 --- a/CoughSync/CoughView/CoughModelView.swift +++ b/CoughSync/CoughView/CoughModelView.swift @@ -12,52 +12,64 @@ // Created by Ethan Bell on 12/2/2025. // +import Spezi import SwiftUI + 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..40e6f7f 100644 --- a/CoughSync/Dashboard/Dashboard.swift +++ b/CoughSync/Dashboard/Dashboard.swift @@ -20,23 +20,29 @@ import SpeziSchedulerUI import SpeziViews import SwiftUI - struct Dashboard: View { + // 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 viewModel: CoughDetectionViewModel? @State private var previousCoughCount: Int = 0 + // MARK: - Body 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,14 +51,24 @@ 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 } } } + // MARK: - Initializers + init(presentingAccount: Binding) { + self._presentingAccount = presentingAccount + } + + // MARK: - Methods @ViewBuilder private func coughSummaryCard() -> some View { VStack { @@ -61,7 +77,7 @@ struct Dashboard: View { Text("Today") .font(.headline) .foregroundColor(.primary) - Text("\(viewModel.coughCount) ") + Text("\(viewModel?.coughCount ?? 0) ") .font(.largeTitle) .bold() .foregroundColor(.blue) @@ -112,7 +128,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 5534543..288d8db 100644 --- a/CoughSync/Resources/Localizable.xcstrings +++ b/CoughSync/Resources/Localizable.xcstrings @@ -352,6 +352,9 @@ } } } + }, + "Loading..." : { + }, "Next" : { "localizations" : {