// // WatchMainViewMoel.swift // Werkout_watch Watch App // // Created by Trey Tartt on 6/22/23. // import Foundation import WatchConnectivity import SwiftUI import HealthKit class WatchMainViewModel: NSObject, ObservableObject { static let shared = WatchMainViewModel() var session: WCSession @Published var isInWorkout = false @Published var watchPackageModel = WatchMainViewModel.defualtPackageModle @Published var heartValue: Int? static var defualtPackageModle: WatchPackageModel { WatchPackageModel(currentExerciseName: "", currentTimeLeft: -1, workoutStartDate: Date()) } let healthStore = HKHealthStore() var hkWorkoutSession: HKWorkoutSession? var hkBuilder: HKLiveWorkoutBuilder? var heartRates = [Int]() override init() { session = WCSession.default super.init() session.delegate = self session.activate() autorizeHealthKit() } func autorizeHealthKit() { let healthKitTypes: Set = [ HKObjectType.quantityType(forIdentifier: .heartRate)!, HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!, HKObjectType.quantityType(forIdentifier: .oxygenSaturation)!, HKQuantityType.workoutType() ] healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { (succ, error) in if !succ { fatalError("Error requesting authorization from health store: \(String(describing: error)))") } } } func nextExercise() { let nextExerciseAction = WatchActions.nextExercise let data = try! JSONEncoder().encode(nextExerciseAction) send(data) } func restartExercise() { let nextExerciseAction = WatchActions.restartExercise let data = try! JSONEncoder().encode(nextExerciseAction) send(data) } func previousExercise() { let nextExerciseAction = WatchActions.previousExercise let data = try! JSONEncoder().encode(nextExerciseAction) send(data) } func completeWorkout() { let nextExerciseAction = WatchActions.stopWorkout let data = try! JSONEncoder().encode(nextExerciseAction) send(data) } func pauseWorkout() { let nextExerciseAction = WatchActions.pauseWorkout let data = try! JSONEncoder().encode(nextExerciseAction) send(data) } } extension WatchMainViewModel { func initWorkout() -> Bool { let configuration = HKWorkoutConfiguration() configuration.activityType = .functionalStrengthTraining configuration.locationType = .indoor do { hkWorkoutSession = try HKWorkoutSession(healthStore: healthStore, configuration: configuration) hkBuilder = hkWorkoutSession?.associatedWorkoutBuilder() } catch { fatalError("Unable to create the workout session!") } guard let hkWorkoutSession = hkWorkoutSession, let hkBuilder = hkBuilder else { return false } // Setup session and builder. hkWorkoutSession.delegate = self hkBuilder.delegate = self /// Set the workout builder's data source. hkBuilder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration) return true } func startWorkout() { // Initialize our workout if initWorkout() { guard let hkWorkoutSession = hkWorkoutSession, let hkBuilder = hkBuilder else { return } // Start the workout session and begin data collection hkWorkoutSession.startActivity(with: Date()) hkBuilder.beginCollection(withStart: Date()) { (succ, error) in if !succ { fatalError("Error beginning collection from builder: \(String(describing: error)))") } } isInWorkout = true } else { print("didn not init workout") } } func stopWorkout(sendDetails: Bool) { guard let hkWorkoutSession = hkWorkoutSession, let hkBuilder = hkBuilder else { return } hkWorkoutSession.end() hkBuilder.endCollection(withEnd: Date()) { (success, error) in hkBuilder.finishWorkout { (workout, error) in DispatchQueue.main.async() { self.hkWorkoutSession = nil self.hkBuilder = nil self.heartRates.removeAll() self.isInWorkout = false guard let id = workout?.uuid else { return } let watchFinishWorkoutModel = WatchFinishWorkoutModel(healthKitUUID: id) let data = try! JSONEncoder().encode(watchFinishWorkoutModel) let watchAction = WatchActions.workoutComplete(data) let watchActionData = try! JSONEncoder().encode(watchAction) if sendDetails { self.send(watchActionData) } } } } } } extension WatchMainViewModel: WCSessionDelegate { func session(_ session: WCSession, didReceiveMessageData messageData: Data) { if let model = try? JSONDecoder().decode(PhoneToWatchActions.self, from: messageData) { DispatchQueue.main.async { switch model { case .inExercise(let data): if self.isInWorkout == false { self.startWorkout() } self.watchPackageModel = data case .reset: self.isInWorkout = false self.watchPackageModel = WatchMainViewModel.defualtPackageModle self.stopWorkout(sendDetails: false) case .endWorkout: self.isInWorkout = false self.watchPackageModel = WatchMainViewModel.defualtPackageModle self.stopWorkout(sendDetails: true) } } } } func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { print("activation did complete") } 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))") } } } extension WatchMainViewModel: HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate { func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) { print("[workoutSession] Changed State: \(toState.rawValue)") } func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) { print("[workoutSession] Encountered an error: \(error)") } 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)") } }