This commit is contained in:
Trey t
2024-06-19 21:07:52 -05:00
parent bba2ce8603
commit c101da4a4d
5 changed files with 209 additions and 152 deletions

View File

@@ -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 = "<group>"; };
1C0494822C23C56E003D18BB /* WatchMainViewModel+WCSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WatchMainViewModel+WCSessionDelegate.swift"; sourceTree = "<group>"; };
1C31C8822A53AE3E00350540 /* short_beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = short_beep.m4a; sourceTree = "<group>"; };
1C31C8832A53AE3E00350540 /* long_beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = long_beep.m4a; sourceTree = "<group>"; };
1C31C8862A55B2CC00350540 /* PlayerUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerUIView.swift; sourceTree = "<group>"; };
@@ -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;
};

View File

@@ -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))")
}
}

View File

@@ -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)
}
}
}

View File

@@ -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<HKSampleType>) {
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)")
}
}

View File

@@ -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<HKSampleType>) {
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)")
}
}