Files
WerkoutIOS/Werkout_ios/BridgeModule.swift
Trey t 8acc8012cb WIP
2023-07-03 21:49:17 -05:00

291 lines
9.6 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 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
@Published var currentExerciseTimeLeft: Int = 0
@Published var currentExercise: ExerciseElement?
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 startTimerWith(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 nextExercise() {
currentExerciseIdx += 1
if let currentWorkout = currentWorkout {
if currentExerciseIdx < currentWorkout.exercises.count {
let nextExercise = currentWorkout.exercises[currentExerciseIdx]
updateCurrent(exercise: nextExercise)
} else {
completeWorkout()
}
}
}
@objc func addOneToWorkoutRunTime() {
currentWorkoutRunTimeInSeconds += 1
}
func updateCurrent(exercise: ExerciseElement) {
DispatchQueue.main.async {
self.currentExercise = exercise
if let duration = exercise.duration {
self.startTimerWith(duration: duration)
}
}
}
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, .allowAirPlay])
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, .allowAirPlay])
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?()
}
}
}
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))")
}
}
}