From c101da4a4d8c7c78f6c65f3950b9bc9e97d36913 Mon Sep 17 00:00:00 2001 From: Trey t Date: Wed, 19 Jun 2024 21:07:52 -0500 Subject: [PATCH] WIP --- iphone/Werkout_ios.xcodeproj/project.pbxproj | 8 + iphone/Werkout_ios/BridgeModule.swift | 33 ++-- ...WatchMainViewModel+WCSessionDelegate.swift | 55 +++++++ .../WatchMainViewModel+WorkoutActions.swift | 124 +++++++++++++++ .../WatchMainViewModel.swift | 141 +----------------- 5 files changed, 209 insertions(+), 152 deletions(-) create mode 100644 iphone/Werkout_watch Watch App/WatchMainViewModel+WCSessionDelegate.swift create mode 100644 iphone/Werkout_watch Watch App/WatchMainViewModel+WorkoutActions.swift diff --git a/iphone/Werkout_ios.xcodeproj/project.pbxproj b/iphone/Werkout_ios.xcodeproj/project.pbxproj index 62a11df..6aa6699 100644 --- a/iphone/Werkout_ios.xcodeproj/project.pbxproj +++ b/iphone/Werkout_ios.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1C0494812C23C53E003D18BB /* WatchMainViewModel+WorkoutActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0494802C23C53E003D18BB /* WatchMainViewModel+WorkoutActions.swift */; }; + 1C0494832C23C56E003D18BB /* WatchMainViewModel+WCSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0494822C23C56E003D18BB /* WatchMainViewModel+WCSessionDelegate.swift */; }; 1C31C8842A53AE3E00350540 /* short_beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 1C31C8822A53AE3E00350540 /* short_beep.m4a */; }; 1C31C8852A53AE3E00350540 /* long_beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 1C31C8832A53AE3E00350540 /* long_beep.m4a */; }; 1C31C8872A55B2CC00350540 /* PlayerUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C31C8862A55B2CC00350540 /* PlayerUIView.swift */; }; @@ -144,6 +146,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1C0494802C23C53E003D18BB /* WatchMainViewModel+WorkoutActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WatchMainViewModel+WorkoutActions.swift"; sourceTree = ""; }; + 1C0494822C23C56E003D18BB /* WatchMainViewModel+WCSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WatchMainViewModel+WCSessionDelegate.swift"; sourceTree = ""; }; 1C31C8822A53AE3E00350540 /* short_beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = short_beep.m4a; sourceTree = ""; }; 1C31C8832A53AE3E00350540 /* long_beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = long_beep.m4a; sourceTree = ""; }; 1C31C8862A55B2CC00350540 /* PlayerUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerUIView.swift; sourceTree = ""; }; @@ -495,6 +499,8 @@ 1C5190D32A59AEDE00885849 /* MainWatchView.swift */, 1C5190D12A59ACA400885849 /* WatchControlView.swift */, 1CF65AB52A4532940042FFBD /* WatchMainViewModel.swift */, + 1C0494802C23C53E003D18BB /* WatchMainViewModel+WorkoutActions.swift */, + 1C0494822C23C56E003D18BB /* WatchMainViewModel+WCSessionDelegate.swift */, 1CF65A952A452D270042FFBD /* Werkout_watchApp.swift */, 1CF65A992A452D290042FFBD /* Assets.xcassets */, 1CF65A9B2A452D290042FFBD /* Preview Content */, @@ -717,6 +723,7 @@ 1C4AFF162A60F27E0027710B /* ThotStyle.swift in Sources */, 1C4AFF212A8801090027710B /* AudioQueue.swift in Sources */, 1CF65AB12A452E1A0042FFBD /* BridgeModule.swift in Sources */, + 1C0494832C23C56E003D18BB /* WatchMainViewModel+WCSessionDelegate.swift in Sources */, 1CF65AAA2A452D9C0042FFBD /* RegisteredUser.swift in Sources */, 1CF65AB62A4532940042FFBD /* WatchMainViewModel.swift in Sources */, 1CD0C6682A5CA1A200970E52 /* BaseURLs.swift in Sources */, @@ -728,6 +735,7 @@ 1C5190D22A59ACA400885849 /* WatchControlView.swift in Sources */, 1C5190D42A59AEDE00885849 /* MainWatchView.swift in Sources */, 1CF65AB42A4530200042FFBD /* WatchPackageModel.swift in Sources */, + 1C0494812C23C53E003D18BB /* WatchMainViewModel+WorkoutActions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iphone/Werkout_ios/BridgeModule.swift b/iphone/Werkout_ios/BridgeModule.swift index 41aa3f1..4fdf7ed 100644 --- a/iphone/Werkout_ios/BridgeModule.swift +++ b/iphone/Werkout_ios/BridgeModule.swift @@ -54,6 +54,7 @@ class BridgeModule: NSObject, ObservableObject { var audioPlayer: AVAudioPlayer? var avPlayer: AVPlayer? + private let session: WCSession = WCSession.default func start(workout: Workout) { currentExerciseInfo.complete = { @@ -73,8 +74,8 @@ class BridgeModule: NSObject, ObservableObject { isInWorkout = true if WCSession.isSupported() { - WCSession.default.delegate = self - WCSession.default.activate() + session.delegate = self + session.activate() } } } @@ -277,15 +278,19 @@ extension BridgeModule: WCSessionDelegate { func sendResetToWatch() { let watchModel = PhoneToWatchActions.reset let data = try! JSONEncoder().encode(watchModel) - send(data) + + // user transferUserInfo b/c its guranteed to reach + // and end the workout + self.session.transferUserInfo(["package": data]) } func sendWorkoutCompleteToWatch() { - if WCSession.default.isReachable { - let model = PhoneToWatchActions.endWorkout - let data = try! JSONEncoder().encode(model) - send(data) - } + let model = PhoneToWatchActions.endWorkout + let data = try! JSONEncoder().encode(model) + + // user transferUserInfo b/c its guranteed to reach + // and end the workout + self.session.transferUserInfo(["package": data]) } func sendCurrentExerciseToWatch() { @@ -349,12 +354,14 @@ extension BridgeModule: WCSessionDelegate { let workoutConfiguration = HKWorkoutConfiguration() workoutConfiguration.activityType = .functionalStrengthTraining workoutConfiguration.locationType = .indoor - if WCSession.isSupported(), WCSession.default.activationState == .activated, WCSession.default.isWatchAppInstalled { + if WCSession.isSupported(), session.activationState == .activated, session.isWatchAppInstalled { HKHealthStore().startWatchApp(with: workoutConfiguration, completion: { (success, error) in print(error.debugDescription) }) } #endif + @unknown default: + print("default") } } #if os(iOS) @@ -367,19 +374,19 @@ extension BridgeModule: WCSessionDelegate { } #endif func send(_ data: Data) { - guard WCSession.default.activationState == .activated else { + guard session.activationState == .activated else { return } #if os(iOS) - guard WCSession.default.isWatchAppInstalled else { + guard session.isWatchAppInstalled else { return } #else - guard WCSession.default.isCompanionAppInstalled else { + guard session.isCompanionAppInstalled else { return } #endif - WCSession.default.sendMessageData(data, replyHandler: nil) { error in + session.sendMessageData(data, replyHandler: nil) { error in print("Cannot send message: \(String(describing: error))") } } diff --git a/iphone/Werkout_watch Watch App/WatchMainViewModel+WCSessionDelegate.swift b/iphone/Werkout_watch Watch App/WatchMainViewModel+WCSessionDelegate.swift new file mode 100644 index 0000000..cc5a8c4 --- /dev/null +++ b/iphone/Werkout_watch Watch App/WatchMainViewModel+WCSessionDelegate.swift @@ -0,0 +1,55 @@ +// +// WatchMainViewModel+WCSessionDelegate.swift +// Werkout_watch Watch App +// +// Created by Trey Tartt on 6/19/24. +// + +import Foundation +import WatchConnectivity +import SwiftUI +import HealthKit + +extension WatchMainViewModel: WCSessionDelegate { + func session(_ session: WCSession, didReceiveMessageData messageData: Data) { + dataToAction(messageData: messageData) + } + + 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 { + func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { + if let messageData = applicationContext["package"] as? Data { + dataToAction(messageData: messageData) + } + + } + + public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { + if let messageData = userInfo["package"] as? Data { + dataToAction(messageData: messageData) + } + } +} diff --git a/iphone/Werkout_watch Watch App/WatchMainViewModel+WorkoutActions.swift b/iphone/Werkout_watch Watch App/WatchMainViewModel+WorkoutActions.swift new file mode 100644 index 0000000..ea7caa7 --- /dev/null +++ b/iphone/Werkout_watch Watch App/WatchMainViewModel+WorkoutActions.swift @@ -0,0 +1,124 @@ +// +// WatchMainViewModel+WorkoutActions.swift +// Werkout_watch Watch App +// +// Created by Trey Tartt on 6/19/24. +// + +import Foundation +import WatchConnectivity +import SwiftUI +import HealthKit + +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: 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)") + } +} diff --git a/iphone/Werkout_watch Watch App/WatchMainViewModel.swift b/iphone/Werkout_watch Watch App/WatchMainViewModel.swift index bc54994..6295816 100644 --- a/iphone/Werkout_watch Watch App/WatchMainViewModel.swift +++ b/iphone/Werkout_watch Watch App/WatchMainViewModel.swift @@ -51,6 +51,7 @@ class WatchMainViewModel: NSObject, ObservableObject { } } + // actions from view func nextExercise() { let nextExerciseAction = WatchActions.nextExercise let data = try! JSONEncoder().encode(nextExerciseAction) @@ -80,87 +81,8 @@ class WatchMainViewModel: NSObject, ObservableObject { 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) { + func dataToAction(messageData: Data) { if let model = try? JSONDecoder().decode(PhoneToWatchActions.self, from: messageData) { DispatchQueue.main.async { switch model { @@ -181,63 +103,4 @@ extension WatchMainViewModel: WCSessionDelegate { } } } - - 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)") - } }