// // WatchWorkout.swift // Werkout_watch Watch App // // Created by Trey Tartt on 7/1/24. // import Foundation import HealthKit class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate { static let shared = WatchWorkout() let healthStore = HKHealthStore() var hkWorkoutSession: HKWorkoutSession! var hkBuilder: HKLiveWorkoutBuilder! var heartRates = [Int]() @Published var heartValue: Int? @Published var isInWorkout = false @Published var isPaused = false private override init() { super.init() setupCore() } func setupCore() { do { let configuration = HKWorkoutConfiguration() configuration.activityType = .functionalStrengthTraining configuration.locationType = .indoor hkWorkoutSession = try HKWorkoutSession(healthStore: healthStore, configuration: configuration) hkBuilder = hkWorkoutSession.associatedWorkoutBuilder() hkWorkoutSession.delegate = self hkBuilder.delegate = self /// Set the workout builder's data source. hkBuilder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration) } catch { fatalError() } } func startWorkout() { if isInWorkout { return } isInWorkout = true setupCore() hkWorkoutSession.startActivity(with: Date()) //WKInterfaceDevice.current().play(.start) } func stopWorkout(sendDetails: Bool) { // hkWorkoutSession.endCurrentActivity(on: Date()) hkWorkoutSession.end() } func togglePaused() { self.isPaused.toggle() } func beginDataCollection() { hkBuilder.beginCollection(withStart: Date()) { (succ, error) in if !succ { fatalError("Error beginning collection from builder: \(String(describing: error)))") } } DispatchQueue.main.async { self.isInWorkout = true } } func getWorkoutBuilderDetails(completion: @escaping (() -> Void)) { DispatchQueue.main.async { self.heartRates.removeAll() self.isInWorkout = false } hkBuilder.endCollection(withEnd: Date()) { (success, error) in if !success || error != nil { completion() return } self.hkBuilder.finishWorkout { (workout, error) in guard let workout = workout else { completion() return } // if !sendDetails { return } DispatchQueue.main.async() { let watchFinishWorkoutModel = WatchFinishWorkoutModel(healthKitUUID: workout.uuid) let data = try! JSONEncoder().encode(watchFinishWorkoutModel) let watchAction = WatchActions.workoutComplete(data) let watchActionData = try! JSONEncoder().encode(watchAction) DataSender.send(watchActionData) completion() } } } } func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) { switch toState { case .notStarted: print("not started") case .running: print("running") startWorkout() beginDataCollection() case .ended: print("ended") getWorkoutBuilderDetails(completion: { self.setupCore() }) case .paused: print("paused") case .prepared: print("prepared") case .stopped: print("stopped") @unknown default: fatalError() } print("[workoutSession] Changed State: \(toState.rawValue)") } func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) { print("[didFailWithError] Workout Builder changed event: \(error.localizedDescription)") // trying to go from ended to something so just end it all if workoutSession.state == .ended { getWorkoutBuilderDetails(completion: { self.setupCore() }) } } func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set) { for type in collectedTypes { guard let quantityType = type as? HKQuantityType else { return } switch quantityType { case HKQuantityType.quantityType(forIdentifier: .heartRate): DispatchQueue.main.async() { let statistics = workoutBuilder.statistics(for: quantityType) let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute()) let value = statistics!.mostRecentQuantity()?.doubleValue(for: heartRateUnit) self.heartValue = Int(Double(round(1 * value!) / 1)) self.heartRates.append(Int(Double(round(1 * value!) / 1))) print("[workoutBuilder] Heart Rate: \(String(describing: self.heartValue))") } default: return } } } func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) { guard let workoutEventType = workoutBuilder.workoutEvents.last?.type else { return } print("[workoutBuilderDidCollectEvent] Workout Builder changed event: \(workoutEventType.rawValue)") } }