355 lines
12 KiB
Swift
355 lines
12 KiB
Swift
//
|
|
// 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 totalCaloire: Float?
|
|
public private(set) var heartRates: [Int]?
|
|
|
|
var audioPlayer: AVAudioPlayer?
|
|
|
|
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 > 0 {
|
|
currentExerciseTimeLeft -= 1
|
|
|
|
if currentExerciseTimeLeft == 0 {
|
|
playFinished()
|
|
} else {
|
|
if currentExerciseTimeLeft <= 3 {
|
|
playBeep()
|
|
}
|
|
}
|
|
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
|
|
|
|
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 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()
|
|
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()
|
|
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))")
|
|
}
|
|
}
|
|
}
|