// // 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 { var session: WCSession @Published var watchPackageModel: WatchPackageModel? @Published var heartValue: Int? 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: HKQuantityTypeIdentifier.heartRate)!, HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!, HKQuantityType.workoutType() ] healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { (succ, error) in if !succ { fatalError("Error requesting authorization from health store: \(String(describing: error)))") } } } private func startHeartRateQuery(quantityTypeIdentifier: HKQuantityTypeIdentifier) { let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()]) let updateHandler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = { query, samples, deletedObjects, queryAnchor, error in guard let samples = samples as? [HKQuantitySample] else { return } } let query = HKAnchoredObjectQuery(type: HKObjectType.quantityType(forIdentifier: quantityTypeIdentifier)!, predicate: devicePredicate, anchor: nil, limit: HKObjectQueryNoLimit, resultsHandler: updateHandler) query.updateHandler = updateHandler healthStore.execute(query) } func nextExercise() { let nextExerciseAction = WatchActions.nextExercise let data = try! JSONEncoder().encode(nextExerciseAction) send(data) } } extension WatchMainViewModel: WCSessionDelegate { func session(_ session: WCSession, didReceiveMessageData messageData: Data) { if let model = try? JSONDecoder().decode(WatchPackageModel.self, from: messageData) { DispatchQueue.main.async { if model.currentTimeLeft == -100 { self.watchPackageModel = nil return } if self.watchPackageModel?.workoutEndDate != nil { self.watchPackageModel = nil self.stopWorkout() } else if self.watchPackageModel == nil { self.startWorkout() } self.watchPackageModel = model } } } func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { } 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 initWorkout() { 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 } // Setup session and builder. hkWorkoutSession.delegate = self hkBuilder.delegate = self /// Set the workout builder's data source. hkBuilder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration) } func startWorkout() { // Initialize our workout 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)))") } } } func stopWorkout() { 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 let totalEnergy = workout?.totalEnergyBurned?.doubleValue(for: .kilocalorie()) ?? -1 let watchFinishWorkoutModel = WatchFinishWorkoutModel(totalBurnedEnergery: totalEnergy, allHeartRates: self.heartRates) let data = try! JSONEncoder().encode(watchFinishWorkoutModel) let watchAction = WatchActions.workoutComplete(data) let watchActionData = try! JSONEncoder().encode(watchAction) self.send(watchActionData) self.heartRates.removeAll() } } } } 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)") } }