// // TimerModule.swift // Werkout_ios // // Created by Trey Tartt on 6/14/23. // import Foundation import WatchConnectivity import AVFoundation import HealthKit enum WatchActions: Codable { case nextExercise case restartExercise case previousExercise case stopWorkout case workoutComplete(Data) } class BridgeModule: NSObject, ObservableObject { private let kMessageKey = "message" static let shared = BridgeModule() @Published var isShowingOnExternalDisplay = false @Published var isInWorkout = false var completedWorkout: (() -> Void)? @Published var currentWorkoutRunTimeInSeconds: Int = -1 private var currentWorkoutRunTimer: Timer? var currentWorkout: Workout? public private(set) var workoutStartDate: Date? private var currentExerciseTimer: Timer? public private(set) var currentExerciseIdx: Int = -1 { didSet { self.currentExercisePositionString = "\(self.currentExerciseIdx+1)/\(self.currentWorkout?.exercises.count ?? 0)" } } @Published var currentExerciseTimeLeft: Int = 0 @Published var currentExercise: ExerciseElement? var currentExercisePositionString: String? private var isWatchConnected = false // workoutEndDate fills out WatchPackageModel.workoutEndDate which // tells the watch app to stop the workout public private(set) var workoutEndDate: Date? public private(set) var totalCaloire: Float? public private(set) var heartRates: [Int]? var audioPlayer: AVAudioPlayer? func start(workout: Workout) { self.currentWorkout = workout currentWorkoutRunTimeInSeconds = 0 currentWorkoutRunTimer?.invalidate() currentWorkoutRunTimer = nil currentExerciseIdx = 0 let exercise = workout.exercises[currentExerciseIdx] updateCurrent(exercise: exercise) startWorkoutTimer() workoutStartDate = Date() isInWorkout = true if WCSession.isSupported() { WCSession.default.delegate = self WCSession.default.activate() } } func goToExerciseAt(index: Int) { guard let currentWorkout = currentWorkout else { return } currentExerciseIdx = index let exercise = currentWorkout.exercises[index] updateCurrent(exercise: exercise) } func resetCurrentWorkout() { DispatchQueue.main.async { self.currentWorkoutRunTimeInSeconds = 0 self.currentWorkoutRunTimer?.invalidate() self.currentWorkoutRunTimer = nil self.currentExerciseTimer?.invalidate() self.currentExerciseTimer = nil self.currentWorkoutRunTimeInSeconds = -1 self.currentExerciseIdx = -1 self.currentExercise = nil self.currentWorkout = nil self.isInWorkout = false self.workoutStartDate = nil self.workoutEndDate = nil } let watchModel = WatchPackageModel(currentExerciseName: "", currentTimeLeft: -100, workoutStartDate: Date()) let data = try! JSONEncoder().encode(watchModel) send(data) } private func startWorkoutTimer() { currentWorkoutRunTimer?.invalidate() currentWorkoutRunTimer = nil currentWorkoutRunTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(addOneToWorkoutRunTime), userInfo: nil, repeats: true) currentWorkoutRunTimer?.fire() } private func startExerciseTimerWith(duration: Int) { DispatchQueue.main.async { self.currentExerciseTimer?.invalidate() self.currentExerciseTimer = nil self.currentExerciseTimeLeft = duration self.currentExerciseTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateCurrentExerciseTimer), userInfo: nil, repeats: true) self.currentExerciseTimer?.fire() } } @objc func updateCurrentExerciseTimer() { if currentExerciseTimeLeft > 0 { currentExerciseTimeLeft -= 1 let watchModel = WatchPackageModel(currentExerciseName: currentExercise?.exercise.name ?? "-", currentTimeLeft: currentExerciseTimeLeft, workoutStartDate: workoutStartDate ?? Date()) let data = try! JSONEncoder().encode(watchModel) send(data) if currentExerciseTimeLeft == 0 { playFinished() } else { if currentExerciseTimeLeft <= 4 { playBeep() } } } else { nextExercise() } } func pauseWorkout() { if let _ = currentExerciseTimer { currentExerciseTimer?.invalidate() currentExerciseTimer = nil } else { startExerciseTimerWith(duration: currentExerciseTimeLeft) } } func nextExercise() { currentExerciseIdx += 1 if let currentWorkout = currentWorkout { if currentExerciseIdx < currentWorkout.exercises.count { let nextExercise = currentWorkout.exercises[currentExerciseIdx] updateCurrent(exercise: nextExercise) } else { completeWorkout() } } } func previousExercise() { currentExerciseIdx -= 1 if currentExerciseIdx < 0 { currentExerciseIdx = 0 } if let currentWorkout = currentWorkout { if currentExerciseIdx < currentWorkout.exercises.count { let nextExercise = currentWorkout.exercises[currentExerciseIdx] updateCurrent(exercise: nextExercise) } else { completeWorkout() } } } func restartExercise() { if let currentWorkout = currentWorkout { if currentExerciseIdx < currentWorkout.exercises.count { let nextExercise = currentWorkout.exercises[currentExerciseIdx] updateCurrent(exercise: nextExercise) } } } @objc func addOneToWorkoutRunTime() { currentWorkoutRunTimeInSeconds += 1 } func updateCurrent(exercise: ExerciseElement) { DispatchQueue.main.async { self.currentExerciseTimer?.invalidate() self.currentExerciseTimer = nil self.currentExercise = exercise if let duration = exercise.duration, duration > 0 { print(duration) self.startExerciseTimerWith(duration: duration) } else { var intWatchDispaly = -1 if let reps = self.currentExercise?.reps, reps > 0 { intWatchDispaly = reps } // if not a timer we need to set the watch display with number of reps // if timer it will set when timer updates let watchModel = WatchPackageModel(currentExerciseName: self.currentExercise?.exercise.name ?? "-", currentTimeLeft: intWatchDispaly, workoutStartDate: self.workoutStartDate ?? Date()) let data = try! JSONEncoder().encode(watchModel) self.send(data) } } } func completeWorkout() { workoutEndDate = Date() //if connected to watch if WCSession.default.isReachable { self.sendWorkoutCompleteToWatch() } else { completedWorkout?() } } func playBeep() { #if os(iOS) if let path = Bundle.main.path(forResource: "short_beep", ofType: "m4a") { do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers]) try AVAudioSession.sharedInstance().setActive(true) audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) audioPlayer?.play() } catch { print("ERROR") } } #endif } func playFinished() { #if os(iOS) if let path = Bundle.main.path(forResource: "long_beep", ofType: "m4a") { do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers]) try AVAudioSession.sharedInstance().setActive(true) audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) audioPlayer?.play() } catch { print("ERROR") } } #endif } } extension BridgeModule: WCSessionDelegate { func sendWorkoutCompleteToWatch() { let watchModel = WatchPackageModel(currentExerciseName: currentExercise?.exercise.name ?? "-", currentTimeLeft: currentExerciseTimeLeft, workoutStartDate: workoutStartDate ?? Date(), workoutEndDate: Date()) let data = try! JSONEncoder().encode(watchModel) send(data) } func session(_ session: WCSession, didReceiveMessageData messageData: Data) { if let model = try? JSONDecoder().decode(WatchActions.self, from: messageData) { switch model { case .nextExercise: nextExercise() case .workoutComplete(let data): let model = try! JSONDecoder().decode(WatchFinishWorkoutModel.self, from: data) totalCaloire = Float(model.totalBurnedEnergery) heartRates = model.allHeartRates completedWorkout?() case .restartExercise: restartExercise() case .previousExercise: previousExercise() case .stopWorkout: completeWorkout() } } } func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { switch activationState { case .notActivated: print("notActivated") case .inactive: print("inactive") case .activated: print("activated") #if os(iOS) let workoutConfiguration = HKWorkoutConfiguration() workoutConfiguration.activityType = .functionalStrengthTraining workoutConfiguration.locationType = .indoor if WCSession.isSupported(), WCSession.default.activationState == .activated, WCSession.default.isWatchAppInstalled { HKHealthStore().startWatchApp(with: workoutConfiguration, completion: { (success, error) in print(error.debugDescription) }) } #endif } } #if os(iOS) func sessionDidBecomeInactive(_ session: WCSession) { } func sessionDidDeactivate(_ session: WCSession) { session.activate() } #endif func send(_ data: Data) { guard WCSession.default.activationState == .activated else { return } #if os(iOS) guard WCSession.default.isWatchAppInstalled else { return } #else guard WCSession.default.isCompanionAppInstalled else { return } #endif WCSession.default.sendMessageData(data, replyHandler: nil) { error in print("Cannot send message: \(String(describing: error))") } } }