// // 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 pauseWorkout case workoutComplete(Data) } enum PhoneToWatchActions: Codable { case inExercise(WatchPackageModel) case reset case endWorkout } 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? public private(set) var workoutStartDate: Date? private var currentExerciseTimer: Timer? @Published public private(set) var currentExerciseInfo = CurrentWorkoutInfo() @Published var previewWorkout: Workout? @Published var currentExerciseTimeLeft: Int = 0 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 healthKitUUID: UUID? var audioPlayer: AVAudioPlayer? var avPlayer: AVPlayer? func start(workout: Workout) { currentExerciseInfo.complete = { self.completeWorkout() } currentExerciseInfo.start(workout: workout) currentWorkoutRunTimeInSeconds = 0 currentWorkoutRunTimer?.invalidate() currentWorkoutRunTimer = nil if let superetExercise = currentExerciseInfo.currentExercise { updateCurrent(exercise: superetExercise) startWorkoutTimer() workoutStartDate = Date() isInWorkout = true if WCSession.isSupported() { WCSession.default.delegate = self WCSession.default.activate() } } } func goToExerciseAt(section: Int, row: Int) { if let superetExercise = currentExerciseInfo.goToWorkoutAt(supersetIndex: section, exerciseIndex: row) { updateCurrent(exercise: superetExercise) } } func resetCurrentWorkout() { DispatchQueue.main.async { self.currentWorkoutRunTimeInSeconds = 0 self.currentWorkoutRunTimer?.invalidate() self.currentWorkoutRunTimer = nil self.currentExerciseTimer?.invalidate() self.currentExerciseTimer = nil self.currentWorkoutRunTimeInSeconds = -1 self.currentExerciseInfo.reset() self.isInWorkout = false self.workoutStartDate = nil self.workoutEndDate = nil } sendResetToWatch() } 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 > 1 { currentExerciseTimeLeft -= 1 if let currentExercise = currentExerciseInfo.allSupersetExecercise, let audioQueues = currentExercise.audioQueues { if let audioQueue = audioQueues.first(where: { $0.playAt == currentExerciseTimeLeft }) { switch audioQueue.audioType { case .shortBeep: playBeep() case .finishBeep: playFinished() case .remoteURL(let url): playRemoteAudio(fromURL: url) } } } sendCurrentExerciseToWatch() } else { nextExercise() } } func pauseWorkout() { if let _ = currentExerciseTimer { currentExerciseTimer?.invalidate() currentExerciseTimer = nil } else { startExerciseTimerWith(duration: currentExerciseTimeLeft) } } func nextExercise() { if let nextSupersetExercise = currentExerciseInfo.nextExercise { updateCurrent(exercise: nextSupersetExercise) } else { completeWorkout() } } func previousExercise() { if let nextSupersetExercise = currentExerciseInfo.previousExercise { updateCurrent(exercise: nextSupersetExercise) } else { completeWorkout() } } func restartExercise() { if let currentExercise = currentExerciseInfo.currentExercise { updateCurrent(exercise: currentExercise) } } @objc func addOneToWorkoutRunTime() { currentWorkoutRunTimeInSeconds += 1 } func updateCurrent(exercise: SupersetExercise) { DispatchQueue.main.async { self.currentExerciseTimer?.invalidate() self.currentExerciseTimer = nil if let duration = exercise.duration, duration > 0 { self.startExerciseTimerWith(duration: duration) } self.sendCurrentExerciseToWatch() } } func completeWorkout() { self.currentExerciseTimer?.invalidate() self.currentExerciseTimer = nil self.isInWorkout = false workoutEndDate = Date() //if connected to watch if WCSession.default.isReachable { self.sendWorkoutCompleteToWatch() } else { completedWorkout?() } } func playRemoteAudio(fromURL url: URL) { #if os(iOS) let playerItem = AVPlayerItem(url: url) do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers]) try AVAudioSession.sharedInstance().setActive(true) avPlayer = AVPlayer(playerItem: playerItem) avPlayer?.play() } catch { print("ERROR") } #endif } 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 sendResetToWatch() { let watchModel = PhoneToWatchActions.reset let data = try! JSONEncoder().encode(watchModel) send(data) } func sendWorkoutCompleteToWatch() { let model = PhoneToWatchActions.endWorkout let data = try! JSONEncoder().encode(model) send(data) } func sendCurrentExerciseToWatch() { if let currentExercise = currentExerciseInfo.currentExercise, let duration = currentExercise.duration , duration > 0 { let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentTimeLeft: currentExerciseTimeLeft, workoutStartDate: workoutStartDate ?? Date()) let model = PhoneToWatchActions.inExercise(watchModel) let data = try! JSONEncoder().encode(model) send(data) } else { if let currentExercise = currentExerciseInfo.currentExercise, let reps = currentExercise.reps, reps > 0 { // 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: currentExercise.exercise.name, currentTimeLeft: reps, workoutStartDate: self.workoutStartDate ?? Date()) let model = PhoneToWatchActions.inExercise(watchModel) let data = try! JSONEncoder().encode(model) self.send(data) } } } func session(_ session: WCSession, didReceiveMessageData messageData: Data) { if let model = try? JSONDecoder().decode(WatchActions.self, from: messageData) { switch model { case .nextExercise: nextExercise() playFinished() case .workoutComplete(let data): let model = try! JSONDecoder().decode(WatchFinishWorkoutModel.self, from: data) healthKitUUID = model.healthKitUUID completedWorkout?() case .restartExercise: restartExercise() case .previousExercise: previousExercise() case .stopWorkout: completeWorkout() case .pauseWorkout: pauseWorkout() } } } 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))") } } }