209 lines
7.5 KiB
Swift
209 lines
7.5 KiB
Swift
//
|
|
// 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<HKSampleType>) {
|
|
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)")
|
|
}
|
|
}
|