Stabilize iOS/watchOS/tvOS apps and add cross-platform audit remediation

This commit is contained in:
Trey t
2026-02-11 12:54:40 -06:00
parent e40275e694
commit acce712261
77 changed files with 2940 additions and 765 deletions

View File

@@ -7,13 +7,16 @@
import Foundation
import HealthKit
import os
class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate {
static let shared = WatchWorkout()
let healthStore = HKHealthStore()
var hkWorkoutSession: HKWorkoutSession!
var hkBuilder: HKLiveWorkoutBuilder!
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
@@ -21,39 +24,51 @@ class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLive
private override init() {
super.init()
setupCore()
_ = setupCore()
}
func 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
hkBuilder = hkWorkoutSession?.associatedWorkoutBuilder()
hkWorkoutSession?.delegate = self
hkBuilder?.delegate = self
/// Set the workout builder's data source.
hkBuilder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
workoutConfiguration: configuration)
hkBuilder?.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
workoutConfiguration: configuration)
return true
} catch {
fatalError()
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
setupCore()
shouldSendWorkoutDetails = true
hkWorkoutSession.startActivity(with: Date())
//WKInterfaceDevice.current().play(.start)
}
func stopWorkout(sendDetails: Bool) {
shouldSendWorkoutDetails = sendDetails
// hkWorkoutSession.endCurrentActivity(on: Date())
hkWorkoutSession.end()
hkWorkoutSession?.end()
}
func togglePaused() {
@@ -61,9 +76,19 @@ class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLive
}
func beginDataCollection() {
guard let hkBuilder = hkBuilder else {
DispatchQueue.main.async {
self.isInWorkout = false
}
return
}
hkBuilder.beginCollection(withStart: Date()) { (succ, error) in
if !succ {
fatalError("Error beginning collection from builder: \(String(describing: error)))")
self.logger.error("Error beginning workout collection: \(String(describing: error), privacy: .public)")
DispatchQueue.main.async {
self.isInWorkout = false
}
}
}
DispatchQueue.main.async {
@@ -72,6 +97,15 @@ class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLive
}
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
@@ -83,19 +117,24 @@ class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLive
return
}
self.hkBuilder.finishWorkout { (workout, error) in
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)
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()
}
}
@@ -105,30 +144,30 @@ class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLive
func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
switch toState {
case .notStarted:
print("not started")
logger.info("Workout state notStarted")
case .running:
print("running")
logger.info("Workout state running")
startWorkout()
beginDataCollection()
case .ended:
print("ended")
logger.info("Workout state ended")
getWorkoutBuilderDetails(completion: {
self.setupCore()
})
case .paused:
print("paused")
logger.info("Workout state paused")
case .prepared:
print("prepared")
logger.info("Workout state prepared")
case .stopped:
print("stopped")
logger.info("Workout state stopped")
@unknown default:
fatalError()
logger.error("Unknown workout state: \(toState.rawValue, privacy: .public)")
}
print("[workoutSession] Changed State: \(toState.rawValue)")
logger.info("Workout session changed state: \(toState.rawValue, privacy: .public)")
}
func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
print("[didFailWithError] Workout Builder changed event: \(error.localizedDescription)")
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: {
@@ -140,26 +179,30 @@ class WatchWorkout: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLive
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return
continue
}
switch quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
DispatchQueue.main.async() {
let statistics = workoutBuilder.statistics(for: quantityType)
guard let statistics = workoutBuilder.statistics(for: quantityType),
let quantity = statistics.mostRecentQuantity() else {
return
}
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))")
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:
return
continue
}
}
}
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
guard let workoutEventType = workoutBuilder.workoutEvents.last?.type else { return }
print("[workoutBuilderDidCollectEvent] Workout Builder changed event: \(workoutEventType.rawValue)")
logger.info("Workout builder event: \(workoutEventType.rawValue, privacy: .public)")
}
}