// // WatchWorkout.swift // Werkout_watch Watch App // // Created by Trey Tartt on 7/1/24. // import Foundation import HealthKit import os class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate { static let shared = WatchWorkout() let healthStore = HKHealthStore() private let logger = Logger(subsystem: "com.werkout.watch", category: "workout") var hkWorkoutSession: HKWorkoutSession? var hkBuilder: HKLiveWorkoutBuilder? var heartRates = [Int]() private var shouldSendWorkoutDetails = true @Published var heartValue: Int? @Published var isInWorkout = false @Published var isPaused = false private override init() { super.init() _ = setupCore() } @discardableResult func setupCore() -> Bool { 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) return true } catch { logger.error("Failed to configure workout session: \(error.localizedDescription, privacy: .public)") hkWorkoutSession = nil hkBuilder = nil return false } } func startWorkout() { if isInWorkout { return } guard setupCore(), let hkWorkoutSession = hkWorkoutSession else { isInWorkout = false return } isInWorkout = true shouldSendWorkoutDetails = true hkWorkoutSession.startActivity(with: Date()) //WKInterfaceDevice.current().play(.start) } func stopWorkout(sendDetails: Bool) { shouldSendWorkoutDetails = sendDetails // hkWorkoutSession.endCurrentActivity(on: Date()) hkWorkoutSession?.end() } func togglePaused() { self.isPaused.toggle() } func beginDataCollection() { guard let hkBuilder = hkBuilder else { DispatchQueue.main.async { self.isInWorkout = false } return } hkBuilder.beginCollection(withStart: Date()) { (succ, error) in if !succ { self.logger.error("Error beginning workout collection: \(String(describing: error), privacy: .public)") DispatchQueue.main.async { self.isInWorkout = false } } } DispatchQueue.main.async { self.isInWorkout = true } } func getWorkoutBuilderDetails(completion: @escaping (() -> Void)) { guard let hkBuilder = hkBuilder else { DispatchQueue.main.async { self.heartRates.removeAll() self.isInWorkout = false } completion() return } DispatchQueue.main.async { self.heartRates.removeAll() self.isInWorkout = false } hkBuilder.endCollection(withEnd: Date()) { (success, error) in if !success || error != nil { completion() return } hkBuilder.finishWorkout { (workout, error) in guard let workout = workout else { completion() return } DispatchQueue.main.async() { if self.shouldSendWorkoutDetails { do { 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) } catch { self.logger.error("Failed to send watch completion payload: \(error.localizedDescription, privacy: .public)") } } completion() } } } } func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) { switch toState { case .notStarted: logger.info("Workout state notStarted") case .running: logger.info("Workout state running") startWorkout() beginDataCollection() case .ended: logger.info("Workout state ended") getWorkoutBuilderDetails(completion: { self.setupCore() }) case .paused: logger.info("Workout state paused") case .prepared: logger.info("Workout state prepared") case .stopped: logger.info("Workout state stopped") @unknown default: logger.error("Unknown workout state: \(toState.rawValue, privacy: .public)") } logger.info("Workout session changed state: \(toState.rawValue, privacy: .public)") } func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) { logger.error("Workout session failed: \(error.localizedDescription, privacy: .public)") // 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 { continue } switch quantityType { case HKQuantityType.quantityType(forIdentifier: .heartRate): DispatchQueue.main.async() { guard let statistics = workoutBuilder.statistics(for: quantityType), let quantity = statistics.mostRecentQuantity() else { return } let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute()) let value = quantity.doubleValue(for: heartRateUnit) let roundedHeartRate = Int(Double(round(1 * value) / 1)) self.heartValue = roundedHeartRate self.heartRates.append(roundedHeartRate) self.logger.debug("Collected heart rate sample: \(roundedHeartRate, privacy: .public)") } default: continue } } } func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) { guard let workoutEventType = workoutBuilder.workoutEvents.last?.type else { return } logger.info("Workout builder event: \(workoutEventType.rawValue, privacy: .public)") } }