diff --git a/hearo/HearoadWatch Watch App/WatchSessionManager.swift b/hearo/HearoadWatch Watch App/WatchSessionManager.swift index e085b83..50d85b8 100644 --- a/hearo/HearoadWatch Watch App/WatchSessionManager.swift +++ b/hearo/HearoadWatch Watch App/WatchSessionManager.swift @@ -9,104 +9,158 @@ import WatchKit import WatchConnectivity class WatchSessionManager: NSObject, ObservableObject, WCSessionDelegate { - static let shared = WatchSessionManager() // 싱글톤 인스턴스 생성 + static let shared = WatchSessionManager() // 싱글톤 인스턴스 생성 + + @Published var alertMessage: String = " " // 기본 메시지 + @Published var isAlerting: Bool = false // 알림 상태 확인 + @Published var isIOSConnected: Bool = false // iOS 연결 상태 + + private override init() { + super.init() - @Published var alertMessage: String = " " // 기본 메시지 - @Published var isAlerting: Bool = false // 알림 상태 확인 - - private override init() { - super.init() - - if WCSession.isSupported() { - WCSession.default.delegate = self - WCSession.default.activate() - } + if WCSession.isSupported() { + let session = WCSession.default + session.delegate = self + session.activate() + } + } + + // MARK: - iOS에서 데이터 수신 + // iOS에서 경고 메시지를 수신하는 메서드 + func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { + if let alert = message["alert"] as? String { + DispatchQueue.main.async { + self.showAlert(with: alert) // 메시지 수신 후 즉각적으로 알림 표시 + print("애플워치 - 메시지 수신: \(alert)") + } + } + } + + func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { + if let alert = applicationContext["alert"] as? String { + DispatchQueue.main.async { + self.showAlert(with: alert) + print("워치 앱 - ApplicationContext 데이터 수신: \(alert)") + } } - // iOS에서 경고 메시지를 수신하는 메서드 - func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { - if let alert = message["alert"] as? String { - DispatchQueue.main.async { - self.showAlert(with: alert) // 메시지 수신 후 즉각적으로 알림 표시 - print("애플워치 - 메시지 수신: \(alert)") - } - } + if let highestConfidenceSound = applicationContext["highestConfidenceSound"] as? String { + DispatchQueue.main.async { + self.showAlert(with: highestConfidenceSound) // 수신한 소리를 알림으로 표시 + print("애플워치 - applicationContext 데이터 수신: \(highestConfidenceSound)") + } + } + } + + // iOS 연결 상태 변경 시 호출 + func sessionReachabilityDidChange(_ session: WCSession) { + DispatchQueue.main.async { + self.isIOSConnected = session.isReachable + print("iOS 연결 상태 변경됨: \(session.isReachable ? "연결됨" : "연결되지 않음")") + + // iOS로 연결 상태 변경 메시지 전송 + self.sendMessageToIOS(key: "connectionStatus", value: session.isReachable ? "connected" : "disconnected") + } + } + + + // MARK: - iOS로 데이터 전송 + func sendMessageToIOS(key: String, value: String) { + guard WCSession.default.activationState == .activated else { + print("WCSession이 활성화되지 않아 데이터 전송 실패") + return } - // 이미지 이름을 반환하는 메서드 - func alertImageName() -> String { - switch alertMessage { - case "Carhorn": - return "Car" // carhorn 이미지 이름 - case "Siren": - return "Siren" // siren 이미지 이름 - case "Bicyclebell": - return "Bicycle" // bicycle 이미지 이름 - default: - return "Car" // 기본 알림 아이콘 - } + if WCSession.default.isReachable { + let message = [key: value] + WCSession.default.sendMessage(message, replyHandler: nil) { error in + print("워치 앱에서 iOS로 메시지 전송 실패: \(error.localizedDescription)") + } + } else { + print("iOS 앱이 연결되지 않아 데이터 전송 실패") } - - // 3초 동안 알림 표시 후 기본 상태로 복구 - func showAlert(with message: String) { - // "녹음 시작 전" 메시지는 무시 - guard message != "녹음 시작 전" else { - print("메시지 무시: \(message)") - return - } - - alertMessage = message - isAlerting = true - - // 강한 진동 알림 발생 - playUrgentHapticPattern() - - // UI 업데이트: 빨간 배경, 아이콘 표시 - DispatchQueue.main.async { - print("UI 업데이트 - 배경색: 빨간색, 메시지: \(message)") - } - - // 3초 후에 기본 상태로 복구 - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - self.resetAlert() - } + } + + func updateApplicationContext(key: String, value: String) { + do { + try WCSession.default.updateApplicationContext([key: value]) + print("워치 앱에서 iOS로 ApplicationContext 전송 성공") + } catch { + print("워치 앱에서 ApplicationContext 전송 실패: \(error.localizedDescription)") } - - // 긴급 상황을 위한 강한 진동 패턴 - func playUrgentHapticPattern() { - // 반복 횟수 및 간격 설정 - let repeatCount = 30 // 진동 반복 횟수 - let interval: TimeInterval = 0.05 // 반복 간격 (0.1초) - - // 반복적으로 강한 진동을 재생하는 패턴 - for i in 0.. String { + switch alertMessage { + case "Carhorn": + return "Car" // carhorn 이미지 이름 + case "Siren": + return "Siren" // siren 이미지 이름 + case "Bicyclebell": + return "Bicycle" // bicycle 이미지 이름 + default: + return "Car" // 기본 알림 아이콘 } + } + + // 3초 동안 알림 표시 후 기본 상태로 복구 + func showAlert(with message: String) { + // "녹음 시작 전" 메시지는 무시 + guard message != "녹음 시작 전" else { + print("메시지 무시: \(message)") + return + } + + alertMessage = message + isAlerting = true - // 기본 상태로 복구하는 메서드 - func resetAlert() { - alertMessage = "인식중" - isAlerting = false - print("UI 상태 복구 - 배경색: 검정, 메시지: 기본 상태") + // 강한 진동 알림 발생 + playUrgentHapticPattern() + + // UI 업데이트: 빨간 배경, 아이콘 표시 + DispatchQueue.main.async { + print("UI 업데이트 - 배경색: 빨간색, 메시지: \(message)") } - // WCSession 활성화 완료 시 호출되는 메서드 - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { - print("애플워치 - WCSession 활성화 완료. 상태: \(activationState.rawValue)") - if let error = error { - print("애플워치 - 활성화 오류: \(error.localizedDescription)") - } + // 3초 후에 기본 상태로 복구 + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + self.resetAlert() } + } + + // 긴급 상황을 위한 강한 진동 패턴 + func playUrgentHapticPattern() { + // 반복 횟수 및 간격 설정 + let repeatCount = 30 // 진동 반복 횟수 + let interval: TimeInterval = 0.05 // 반복 간격 (0.1초) - func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { - if let highestConfidenceSound = applicationContext["highestConfidenceSound"] as? String { - DispatchQueue.main.async { - self.showAlert(with: highestConfidenceSound) // 수신한 소리를 알림으로 표시 - print("애플워치 - applicationContext 데이터 수신: \(highestConfidenceSound)") - } - } + // 반복적으로 강한 진동을 재생하는 패턴 + for i in 0..() + @Published var isRecording = false + @Published var classificationResult: String = "녹음 시작 전" + @Published var confidence: Double = 0.0 + @Published var isWatchConnected: Bool = false // 애플워치 연결 상태를 추적 + + + private var hornSoundDetector: HornSoundDetector + private var appRootManager: AppRootManager + private var cancellables = Set() + private var connectionCheckTimer: Timer? + + init(appRootManager: AppRootManager) { + self.appRootManager = appRootManager + self.hornSoundDetector = HornSoundDetector(appRootManager: appRootManager) + super.init() + setupBindings() + setupWCSession() // WCSession 설정 + startConnectionCheckTimerIfNeeded() - init(appRootManager: AppRootManager) { - self.appRootManager = appRootManager - self.hornSoundDetector = HornSoundDetector(appRootManager: appRootManager) - super.init() - setupBindings() - setupWCSession() // WCSession 설정 - - // NotificationCenter를 통해 detectedSoundNotification 노티피케이션 수신 등록 - NotificationCenter.default.addObserver( - self, - selector: #selector(handleDetectedSoundNotification(_:)), - name: .detectedSoundNotification, - object: nil - ) + // NotificationCenter를 통해 detectedSoundNotification 노티피케이션 수신 등록 + NotificationCenter.default.addObserver( + self, + selector: #selector(handleDetectedSoundNotification(_:)), + name: .detectedSoundNotification, + object: nil + ) + } + + + @objc private func handleDetectedSoundNotification(_ notification: Notification) { + if let sound = notification.userInfo?["sound"] as? String { + print("SoundDetectorViewModel에서 전달받은 sound: \(sound)") + sendWarningToWatch(alert: sound) // 애플워치로 알림 전송 } + } + + deinit { + connectionCheckTimer?.invalidate() + NotificationCenter.default.removeObserver(self, name: .detectedSoundNotification, object: nil) + } + + private func setupBindings() { + hornSoundDetector.$isRecording + .assign(to: \.isRecording, on: self) + .store(in: &cancellables) + // HornSoundDetector에서 Warning 발생 시 직접 Watch로 전달 + hornSoundDetector.$classificationResult + .sink { [weak self] result in + guard let self = self else { return } + print("SoundDetectorViewModel에서 전달받은 classificationResult: \(result)") + self.sendWarningToWatch(alert: result) // Watch에 전달 + } + .store(in: &cancellables) - @objc private func handleDetectedSoundNotification(_ notification: Notification) { - if let sound = notification.userInfo?["sound"] as? String { - print("SoundDetectorViewModel에서 전달받은 sound: \(sound)") - sendWarningToWatch(alert: sound) // 애플워치로 알림 전송 - } - } - deinit { - NotificationCenter.default.removeObserver(self, name: .detectedSoundNotification, object: nil) + hornSoundDetector.$confidence + .assign(to: \.confidence, on: self) + .store(in: &cancellables) + } + + func setupWCSession() { + if WCSession.isSupported() { + let session = WCSession.default + session.delegate = self + session.activate() + print("WCSession 활성화 요청") + updateWatchConnectionState() // 초기 연결 상태 확인 } - - private func setupBindings() { - hornSoundDetector.$isRecording - .assign(to: \.isRecording, on: self) - .store(in: &cancellables) - - // HornSoundDetector에서 Warning 발생 시 직접 Watch로 전달 - hornSoundDetector.$classificationResult - .sink { [weak self] result in - guard let self = self else { return } - print("SoundDetectorViewModel에서 전달받은 classificationResult: \(result)") - self.sendWarningToWatch(alert: result) // Watch에 전달 - } - .store(in: &cancellables) - - hornSoundDetector.$confidence - .assign(to: \.confidence, on: self) - .store(in: &cancellables) - } - - func setupWCSession() { - if WCSession.isSupported() { - let session = WCSession.default - session.delegate = self - session.activate() - print("WCSession 활성화 요청") - updateWatchConnectionState() // 초기 연결 상태 확인 - } - else { - print("WCSession이 지원되지 않습니다.") - } + else { + print("WCSession이 지원되지 않습니다.") } - private func updateWatchConnectionState() { - // 애플워치 연결 상태를 업데이트 - isWatchConnected = WCSession.default.isPaired && WCSession.default.isWatchAppInstalled - print("애플워치 연결 상태 업데이트: \(isWatchConnected ? "연결됨" : "연결되지 않음")") + } + + private func startConnectionCheckTimerIfNeeded() { + // 현재 화면이 WorkingView가 아니라면 타이머 중지 + guard appRootManager.currentRoot == .working else { + stopConnectionCheckTimer() + return } - - func startRecording() { - hornSoundDetector.startRecording() + // 타이머를 중복해서 실행하지 않도록 기존 타이머를 중지 + connectionCheckTimer?.invalidate() + connectionCheckTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in +// guard let self else { return } + self?.updateWatchConnectionState() } - - func stopRecording() { - hornSoundDetector.stopRecording() + print("워치 연결 확인 타이머 시작(현재 화면: \(appRootManager.currentRoot))") + } + + private func stopConnectionCheckTimer() { + connectionCheckTimer?.invalidate() + connectionCheckTimer = nil + print("타이머 중지 (현채 화면: \(appRootManager.currentRoot))") + } + + private func updateWatchConnectionState() { + // 애플워치 연결 상태를 업데이트 + isWatchConnected = WCSession.default.isReachable + print("애플워치 연결 상태 업데이트: \(isWatchConnected ? "연결됨" : "연결되지 않음")") + } + + + func startRecording() { + hornSoundDetector.startRecording() + } + + func stopRecording() { + hornSoundDetector.stopRecording() + } + + private func sendWarningToWatch(alert: String) { + guard WCSession.default.activationState == .activated else { + print("WCSession이 활성화되지 않아 전송 보류: \(alert)") + return } - private func sendWarningToWatch(alert: String) { - guard WCSession.default.activationState == .activated else { - print("WCSession이 활성화되지 않아 전송 보류: \(alert)") - return - } - - if WCSession.default.isReachable { - let message = ["alert": alert] - WCSession.default.sendMessage(message, replyHandler: nil) { error in - print("애플워치로 메시지 전송 실패: \(error.localizedDescription)") - } - } else { - do { - try WCSession.default.updateApplicationContext(["alert": alert]) - print("애플워치에 ApplicationContext로 데이터 전달 성공: \(alert)") - } catch { - print("애플워치 ApplicationContext 데이터 전달 실패: \(error.localizedDescription)") - } - } + if WCSession.default.isReachable { + let message = ["alert": alert] + WCSession.default.sendMessage(message, replyHandler: nil) { error in + print("애플워치로 메시지 전송 실패: \(error.localizedDescription)") + } + } else { + do { + try WCSession.default.updateApplicationContext(["alert": alert]) + print("애플워치에 ApplicationContext로 데이터 전달 성공: \(alert)") + } catch { + print("애플워치 ApplicationContext 데이터 전달 실패: \(error.localizedDescription)") + } } - func sessionWatchStateDidChange(_ session: WCSession) { - DispatchQueue.main.async { - self.updateWatchConnectionState() - } + } + + + + func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { + if let connectionStatus = message["connectionStatus"] as? String { + // print("iOS 앱에서 워치 연결 상태 메시지 수신: \(connectionStatus)") + + // 워치에서 상태 변경 메시지를 받았으므로 상태 업데이트 + DispatchQueue.main.async { + self.updateWatchConnectionState() + } } - - - - // WCSessionDelegate 메서드 - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { - DispatchQueue.main.async { - self.updateWatchConnectionState() - } - - switch activationState { - case .notActivated: - print("WCSession이 활성화되지 않음.") - case .inactive: - print("WCSession이 비활성 상태.") - case .activated: - print("WCSession 활성화 성공.") - @unknown default: - print("알 수 없는 WCSession 상태.") - } - - if let error = error { - print("WCSession 활성화 실패: \(error.localizedDescription)") - } + } + + + // WCSessionDelegate 메서드 + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + DispatchQueue.main.async { + self.updateWatchConnectionState() } - func sessionDidBecomeInactive(_ session: WCSession) { - // 세션이 비활성화되었을 때 로그를 남기거나 필요한 작업을 수행 - print("WCSession이 비활성화되었습니다. 세션 상태: \(session.activationState.rawValue)") + switch activationState { + case .notActivated: + print("WCSession이 활성화되지 않음.") + case .inactive: + print("WCSession이 비활성 상태.") + case .activated: + print("WCSession 활성화 성공.") + @unknown default: + print("알 수 없는 WCSession 상태.") } - func sessionDidDeactivate(_ session: WCSession) { - // 세션이 비활성화된 후 다시 활성화를 준비 - print("WCSession이 비활성화되었습니다. 새로운 세션을 활성화합니다.") - WCSession.default.activate() + if let error = error { + print("WCSession 활성화 실패: \(error.localizedDescription)") } - + } + + func sessionDidBecomeInactive(_ session: WCSession) { + // 세션이 비활성화되었을 때 로그를 남기거나 필요한 작업을 수행 + print("WCSession이 비활성화되었습니다. 세션 상태: \(session.activationState.rawValue)") + } + + func sessionDidDeactivate(_ session: WCSession) { + // 세션이 비활성화된 후 다시 활성화를 준비 + print("WCSession이 비활성화되었습니다. 새로운 세션을 활성화합니다.") + WCSession.default.activate() + } + } diff --git a/hearo/hearo/Sources/Presentations/Working/View/WorkingView.swift b/hearo/hearo/Sources/Presentations/Working/View/WorkingView.swift index 8348ac7..6ccc272 100644 --- a/hearo/hearo/Sources/Presentations/Working/View/WorkingView.swift +++ b/hearo/hearo/Sources/Presentations/Working/View/WorkingView.swift @@ -12,9 +12,14 @@ struct WorkingView: View { @State private var circleOffset: CGFloat = 0 @State private var showArrowAndText: Bool = false @State private var overlayOpacity: Double = 1.0 // 처음에 화면을 덮는 흰색 오버레이 불투명도 + +// @State private var isWatchConnected: Bool = false // 워치 연동 상태 관리 + + @State private var backgroundOpacity: Double = 0.0 // 전환 중 흰색 배경 불투명도 @State private var isTransitioning: Bool = false // 전환 상태 관리 - @State private var isWatchConnected: Bool = false // 워치 연동 상태 관리 + + private let targetOffset: CGFloat = 274 private let minimumOffset: CGFloat = 197 @@ -28,7 +33,9 @@ struct WorkingView: View { Color("Radish") .ignoresSafeArea() + // Lottie 애니메이션과 원형 버튼 + LottieView(animationName: "sound_collection", animationScale: 1) .frame( width: UIScreen.main.bounds.width, @@ -44,7 +51,7 @@ struct WorkingView: View { .offset(y: -307) // 워치 연동 상태에 따른 이미지 변경 - Image(isWatchConnected ? "WatchOn" : "WatchOff") + Image(viewModel.isWatchConnected ? "WatchOn" : "WatchOff") .offset(x: 40, y: -307) } diff --git a/hearo/hearo/Sources/Presentations/Working/ViewModel/WorkingViewModel.swift b/hearo/hearo/Sources/Presentations/Working/ViewModel/WorkingViewModel.swift index 8c899c1..cf97206 100644 --- a/hearo/hearo/Sources/Presentations/Working/ViewModel/WorkingViewModel.swift +++ b/hearo/hearo/Sources/Presentations/Working/ViewModel/WorkingViewModel.swift @@ -38,8 +38,9 @@ class WorkingViewModel: ObservableObject { } private func setupBindings() { - // SoundDetectorViewModel의 isWatchConnected를 관찰 + // `SoundDetectorViewModel`의 `isWatchConnected`를 구독하여 동기화 soundDetectorViewModel.$isWatchConnected + .receive(on: DispatchQueue.main) .assign(to: \.isWatchConnected, on: self) .store(in: &cancellables) }