split files up, fix complete workout screen not showing if remove workout details screen
This commit is contained in:
@@ -9,6 +9,14 @@
|
||||
/* 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 */; };
|
||||
1C0494872C23E7BD003D18BB /* BridgeModule+Watch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0494862C23E7BD003D18BB /* BridgeModule+Watch.swift */; };
|
||||
1C0494882C23E7C5003D18BB /* BridgeModule+Watch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0494862C23E7BD003D18BB /* BridgeModule+Watch.swift */; };
|
||||
1C04948A2C25CB4F003D18BB /* BridgeModule+WorkoutActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0494892C25CB4F003D18BB /* BridgeModule+WorkoutActions.swift */; };
|
||||
1C04948C2C25CB80003D18BB /* AudioEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C04948B2C25CB80003D18BB /* AudioEngine.swift */; };
|
||||
1C04948D2C25CC93003D18BB /* AudioEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C04948B2C25CB80003D18BB /* AudioEngine.swift */; };
|
||||
1C04948E2C25CD3D003D18BB /* BridgeModule+WorkoutActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0494892C25CB4F003D18BB /* BridgeModule+WorkoutActions.swift */; };
|
||||
1C0494932C25CEF0003D18BB /* BridgeModule+Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0494922C25CEF0003D18BB /* BridgeModule+Timer.swift */; };
|
||||
1C0494942C25CEF4003D18BB /* BridgeModule+Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C0494922C25CEF0003D18BB /* BridgeModule+Timer.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 */; };
|
||||
@@ -148,6 +156,10 @@
|
||||
/* 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>"; };
|
||||
1C0494862C23E7BD003D18BB /* BridgeModule+Watch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BridgeModule+Watch.swift"; sourceTree = "<group>"; };
|
||||
1C0494892C25CB4F003D18BB /* BridgeModule+WorkoutActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BridgeModule+WorkoutActions.swift"; sourceTree = "<group>"; };
|
||||
1C04948B2C25CB80003D18BB /* AudioEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEngine.swift; sourceTree = "<group>"; };
|
||||
1C0494922C25CEF0003D18BB /* BridgeModule+Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BridgeModule+Timer.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>"; };
|
||||
@@ -344,9 +356,13 @@
|
||||
1CF65AB22A452F360042FFBD /* WatchPackageModel.swift */,
|
||||
1CF65A822A42347D0042FFBD /* Extensions.swift */,
|
||||
1CF65A272A3972840042FFBD /* Persistence.swift */,
|
||||
1C04948B2C25CB80003D18BB /* AudioEngine.swift */,
|
||||
1CD0C6662A5CA19600970E52 /* BaseURLs.swift */,
|
||||
1C4AFF1A2A65FB190027710B /* CurrentWorkoutInfo.swift */,
|
||||
1CF65A4F2A3A1EA90042FFBD /* BridgeModule.swift */,
|
||||
1C0494922C25CEF0003D18BB /* BridgeModule+Timer.swift */,
|
||||
1C0494862C23E7BD003D18BB /* BridgeModule+Watch.swift */,
|
||||
1C0494892C25CB4F003D18BB /* BridgeModule+WorkoutActions.swift */,
|
||||
1CF65A802A412AA30042FFBD /* DataStore.swift */,
|
||||
1CF65AB92A4894430042FFBD /* UserStore.swift */,
|
||||
1C6BF28E2A56602B00450FD7 /* Keychain.swift */,
|
||||
@@ -565,7 +581,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1430;
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1530;
|
||||
TargetAttributes = {
|
||||
1CF65A212A3972840042FFBD = {
|
||||
CreatedOnToolsVersion = 14.3;
|
||||
@@ -640,6 +656,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C04948A2C25CB4F003D18BB /* BridgeModule+WorkoutActions.swift in Sources */,
|
||||
1CF65A7F2A4129320042FFBD /* Fetchables.swift in Sources */,
|
||||
1CF65A812A412AA30042FFBD /* DataStore.swift in Sources */,
|
||||
1CF65A6B2A3C1EAC0042FFBD /* CreateWorkoutMainView.swift in Sources */,
|
||||
@@ -697,9 +714,12 @@
|
||||
1C485C8C2A49D65600A6F896 /* WorkoutHistoryView.swift in Sources */,
|
||||
1CAF4D952A52180600B00E50 /* PlanWorkoutView.swift in Sources */,
|
||||
1C5190C62A589CC100885849 /* ActionsView.swift in Sources */,
|
||||
1C0494932C25CEF0003D18BB /* BridgeModule+Timer.swift in Sources */,
|
||||
1CC092F52C1FAE7B0004E1E6 /* ShowNextUpView.swift in Sources */,
|
||||
1CF65A5B2A3BF4BE0042FFBD /* Equipment.swift in Sources */,
|
||||
1C4AFF152A60F25F0027710B /* ThotStyle.swift in Sources */,
|
||||
1C0494872C23E7BD003D18BB /* BridgeModule+Watch.swift in Sources */,
|
||||
1C04948C2C25CB80003D18BB /* AudioEngine.swift in Sources */,
|
||||
1C5190C82A589CDA00885849 /* CurrentWorkoutElapsedTimeView.swift in Sources */,
|
||||
1CC092F92C1FB1420004E1E6 /* AllExerciseView.swift in Sources */,
|
||||
1CC093012C20B0E90004E1E6 /* TitleView.swift in Sources */,
|
||||
@@ -719,13 +739,16 @@
|
||||
1CF65A962A452D270042FFBD /* Werkout_watchApp.swift in Sources */,
|
||||
1C4AFF192A65CD6F0027710B /* Superset.swift in Sources */,
|
||||
1CF65AA92A452D9C0042FFBD /* Workout.swift in Sources */,
|
||||
1C0494882C23E7C5003D18BB /* BridgeModule+Watch.swift in Sources */,
|
||||
1CF65AA62A452D9C0042FFBD /* Equipment.swift in Sources */,
|
||||
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 */,
|
||||
1C04948D2C25CC93003D18BB /* AudioEngine.swift in Sources */,
|
||||
1CF65AB62A4532940042FFBD /* WatchMainViewModel.swift in Sources */,
|
||||
1C0494942C25CEF4003D18BB /* BridgeModule+Timer.swift in Sources */,
|
||||
1CD0C6682A5CA1A200970E52 /* BaseURLs.swift in Sources */,
|
||||
1CF65AA72A452D9C0042FFBD /* Muscle.swift in Sources */,
|
||||
1C4AFF1C2A65FB2B0027710B /* CurrentWorkoutInfo.swift in Sources */,
|
||||
@@ -733,6 +756,7 @@
|
||||
1CF65AA52A452D9C0042FFBD /* CompletedWorkout.swift in Sources */,
|
||||
1CF65AA82A452D9C0042FFBD /* Exercise.swift in Sources */,
|
||||
1C5190D22A59ACA400885849 /* WatchControlView.swift in Sources */,
|
||||
1C04948E2C25CD3D003D18BB /* BridgeModule+WorkoutActions.swift in Sources */,
|
||||
1C5190D42A59AEDE00885849 /* MainWatchView.swift in Sources */,
|
||||
1CF65AB42A4530200042FFBD /* WatchPackageModel.swift in Sources */,
|
||||
1C0494812C23C53E003D18BB /* WatchMainViewModel+WorkoutActions.swift in Sources */,
|
||||
@@ -783,9 +807,11 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -841,9 +867,11 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -869,6 +897,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Werkout_ios/Werkout_ios.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Werkout_ios/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = V3PF3M6B6U;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -913,6 +942,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Werkout_ios/Werkout_ios.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Werkout_ios/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = V3PF3M6B6U;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1530"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1530"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
66
iphone/Werkout_ios/AudioEngine.swift
Normal file
66
iphone/Werkout_ios/AudioEngine.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// AudioEngine.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/21/24.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
|
||||
class AudioEngine {
|
||||
// var audioPlayer: AVAudioPlayer?
|
||||
// var avPlayer: AVPlayer?
|
||||
|
||||
static func playRemoteAudio(fromURL url: URL) {
|
||||
#if os(iOS)
|
||||
let playerItem = AVPlayerItem(url: url)
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
let avPlayer = AVPlayer(playerItem: playerItem)
|
||||
avPlayer.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static func playBeep() {
|
||||
#if os(iOS)
|
||||
if let path = Bundle.main.path(forResource: "short_beep", ofType: "m4a") {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
let audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
||||
audioPlayer.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static func playFinished() {
|
||||
#if os(iOS)
|
||||
if let path = Bundle.main.path(forResource: "long_beep", ofType: "m4a") {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
let audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
||||
audioPlayer.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
58
iphone/Werkout_ios/BridgeModule+Timer.swift
Normal file
58
iphone/Werkout_ios/BridgeModule+Timer.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// BridgeModule+Timer.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/21/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension BridgeModule {
|
||||
func startWorkoutTimer() {
|
||||
currentWorkoutRunTimer?.invalidate()
|
||||
currentWorkoutRunTimer = nil
|
||||
currentWorkoutRunTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in
|
||||
self.currentWorkoutRunTimeInSeconds += 1
|
||||
})
|
||||
currentWorkoutRunTimer?.fire()
|
||||
}
|
||||
|
||||
func startExerciseTimerWith(duration: Int) {
|
||||
DispatchQueue.main.async {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
self.currentExerciseTimeLeft = duration
|
||||
self.currentExerciseTimer = Timer.scheduledTimer(timeInterval: 1,
|
||||
target: self,
|
||||
selector: #selector(self.updateCurrentExerciseTimer),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
self.currentExerciseTimer?.fire()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateCurrentExerciseTimer() {
|
||||
if currentExerciseTimeLeft > 1 {
|
||||
currentExerciseTimeLeft -= 1
|
||||
|
||||
if let currentExercise = currentExerciseInfo.allSupersetExecercise, let audioQueues = currentExercise.audioQueues {
|
||||
if let audioQueue = audioQueues.first(where: {
|
||||
$0.playAt == currentExerciseTimeLeft
|
||||
}) {
|
||||
switch audioQueue.audioType {
|
||||
|
||||
case .shortBeep:
|
||||
AudioEngine.playBeep()
|
||||
case .finishBeep:
|
||||
AudioEngine.playFinished()
|
||||
case .remoteURL(let url):
|
||||
AudioEngine.playRemoteAudio(fromURL: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
sendCurrentExerciseToWatch()
|
||||
} else {
|
||||
nextExercise()
|
||||
}
|
||||
}
|
||||
}
|
||||
140
iphone/Werkout_ios/BridgeModule+Watch.swift
Normal file
140
iphone/Werkout_ios/BridgeModule+Watch.swift
Normal file
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// BridgeModule+Watch.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/19/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WatchConnectivity
|
||||
import AVFoundation
|
||||
import HealthKit
|
||||
|
||||
extension BridgeModule: WCSessionDelegate {
|
||||
func sendResetToWatch() {
|
||||
let watchModel = PhoneToWatchActions.reset
|
||||
let data = try! JSONEncoder().encode(watchModel)
|
||||
|
||||
// user transferUserInfo b/c its guranteed to reach
|
||||
// and end the workout
|
||||
self.session.transferUserInfo(["package": data])
|
||||
}
|
||||
|
||||
func sendStartWorkoutToWatch() {
|
||||
let model = PhoneToWatchActions.startWorkout
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
|
||||
// user transferUserInfo b/c its guranteed to reach
|
||||
// and start the workout
|
||||
self.session.transferUserInfo(["package": data])
|
||||
}
|
||||
|
||||
func sendWorkoutCompleteToWatch() {
|
||||
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() {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise,
|
||||
let duration = currentExercise.duration ,
|
||||
duration > 0 {
|
||||
let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name,
|
||||
currentTimeLeft: currentExerciseTimeLeft,
|
||||
workoutStartDate: workoutStartDate ?? Date())
|
||||
let model = PhoneToWatchActions.inExercise(watchModel)
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
send(data)
|
||||
} else {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise,
|
||||
let reps = currentExercise.reps,
|
||||
reps > 0 {
|
||||
|
||||
// if not a timer we need to set the watch display with number of reps
|
||||
// if timer it will set when timer updates
|
||||
let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentTimeLeft: reps, workoutStartDate: self.workoutStartDate ?? Date())
|
||||
let model = PhoneToWatchActions.inExercise(watchModel)
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
self.send(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
|
||||
if let model = try? JSONDecoder().decode(WatchActions.self, from: messageData) {
|
||||
switch model {
|
||||
case .nextExercise:
|
||||
nextExercise()
|
||||
AudioEngine.playFinished()
|
||||
case .workoutComplete(let data):
|
||||
DispatchQueue.main.async {
|
||||
let model = try! JSONDecoder().decode(WatchFinishWorkoutModel.self, from: data)
|
||||
self.healthKitUUID = model.healthKitUUID
|
||||
}
|
||||
case .restartExercise:
|
||||
restartExercise()
|
||||
case .previousExercise:
|
||||
previousExercise()
|
||||
case .stopWorkout:
|
||||
completeWorkout()
|
||||
case .pauseWorkout:
|
||||
pauseWorkout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func session(_ session: WCSession,
|
||||
activationDidCompleteWith activationState: WCSessionActivationState,
|
||||
error: Error?) {
|
||||
switch activationState {
|
||||
case .notActivated:
|
||||
print("notActivated")
|
||||
case .inactive:
|
||||
print("inactive")
|
||||
case .activated:
|
||||
print("activated")
|
||||
#if os(iOS)
|
||||
let workoutConfiguration = HKWorkoutConfiguration()
|
||||
workoutConfiguration.activityType = .functionalStrengthTraining
|
||||
workoutConfiguration.locationType = .indoor
|
||||
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)
|
||||
func sessionDidBecomeInactive(_ session: WCSession) {
|
||||
|
||||
}
|
||||
|
||||
func sessionDidDeactivate(_ session: WCSession) {
|
||||
session.activate()
|
||||
}
|
||||
#endif
|
||||
func send(_ data: Data) {
|
||||
guard session.activationState == .activated else {
|
||||
return
|
||||
}
|
||||
#if os(iOS)
|
||||
guard session.isWatchAppInstalled else {
|
||||
return
|
||||
}
|
||||
#else
|
||||
guard session.isCompanionAppInstalled else {
|
||||
return
|
||||
}
|
||||
#endif
|
||||
session.sendMessageData(data, replyHandler: nil) { error in
|
||||
print("Cannot send message: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
123
iphone/Werkout_ios/BridgeModule+WorkoutActions.swift
Normal file
123
iphone/Werkout_ios/BridgeModule+WorkoutActions.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// BridgeModule+WorkoutActions.swift
|
||||
// Werkout_ios
|
||||
//
|
||||
// Created by Trey Tartt on 6/21/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WatchConnectivity
|
||||
|
||||
extension BridgeModule {
|
||||
func pauseWorkout() {
|
||||
if let _ = currentExerciseTimer {
|
||||
currentExerciseTimer?.invalidate()
|
||||
currentExerciseTimer = nil
|
||||
isPaused = true
|
||||
} else {
|
||||
isPaused = false
|
||||
startExerciseTimerWith(duration: currentExerciseTimeLeft)
|
||||
}
|
||||
}
|
||||
|
||||
func nextExercise() {
|
||||
if let nextSupersetExercise = currentExerciseInfo.goToNextExercise {
|
||||
updateCurrent(exercise: nextSupersetExercise)
|
||||
} else {
|
||||
completeWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
func previousExercise() {
|
||||
if let nextSupersetExercise = currentExerciseInfo.previousExercise {
|
||||
updateCurrent(exercise: nextSupersetExercise)
|
||||
} else {
|
||||
completeWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
func restartExercise() {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise {
|
||||
updateCurrent(exercise: currentExercise)
|
||||
}
|
||||
}
|
||||
|
||||
func updateCurrent(exercise: SupersetExercise) {
|
||||
DispatchQueue.main.async {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
|
||||
if let duration = exercise.duration, duration > 0 {
|
||||
self.startExerciseTimerWith(duration: duration)
|
||||
}
|
||||
self.sendCurrentExerciseToWatch()
|
||||
}
|
||||
}
|
||||
|
||||
func completeWorkout() {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
self.isInWorkout = false
|
||||
|
||||
workoutEndDate = Date()
|
||||
if let completedWorkout = completedWorkout {
|
||||
completedWorkout()
|
||||
self.completedWorkout = nil
|
||||
}
|
||||
}
|
||||
|
||||
func start(workout: Workout) {
|
||||
currentExerciseInfo.complete = {
|
||||
self.completeWorkout()
|
||||
}
|
||||
|
||||
currentExerciseInfo.start(workout: workout)
|
||||
currentWorkoutRunTimeInSeconds = 0
|
||||
currentWorkoutRunTimer?.invalidate()
|
||||
currentWorkoutRunTimer = nil
|
||||
isPaused = false
|
||||
|
||||
sendStartWorkoutToWatch()
|
||||
|
||||
if let superetExercise = currentExerciseInfo.currentExercise {
|
||||
updateCurrent(exercise: superetExercise)
|
||||
startWorkoutTimer()
|
||||
workoutStartDate = Date()
|
||||
isInWorkout = true
|
||||
|
||||
if WCSession.isSupported() {
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func goToExerciseAt(section: Int, row: Int) {
|
||||
if let superetExercise = currentExerciseInfo.goToWorkoutAt(supersetIndex: section,
|
||||
exerciseIndex: row) {
|
||||
updateCurrent(exercise: superetExercise)
|
||||
}
|
||||
}
|
||||
|
||||
var nextExerciseObject: SupersetExercise? {
|
||||
currentExerciseInfo.goToNextExercise
|
||||
}
|
||||
|
||||
func resetCurrentWorkout() {
|
||||
DispatchQueue.main.async {
|
||||
self.currentWorkoutRunTimeInSeconds = 0
|
||||
self.currentWorkoutRunTimer?.invalidate()
|
||||
self.currentWorkoutRunTimer = nil
|
||||
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
|
||||
self.currentWorkoutRunTimeInSeconds = -1
|
||||
self.currentExerciseInfo.reset()
|
||||
|
||||
self.isInWorkout = false
|
||||
self.workoutStartDate = nil
|
||||
self.workoutEndDate = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ enum PhoneToWatchActions: Codable {
|
||||
case inExercise(WatchPackageModel)
|
||||
case reset
|
||||
case endWorkout
|
||||
case startWorkout
|
||||
}
|
||||
|
||||
class BridgeModule: NSObject, ObservableObject {
|
||||
@@ -34,11 +35,11 @@ class BridgeModule: NSObject, ObservableObject {
|
||||
@Published var isInWorkout = false
|
||||
var completedWorkout: (() -> Void)?
|
||||
@Published var currentWorkoutRunTimeInSeconds: Int = -1
|
||||
private var currentWorkoutRunTimer: Timer?
|
||||
var currentWorkoutRunTimer: Timer?
|
||||
|
||||
public private(set) var workoutStartDate: Date?
|
||||
public var workoutStartDate: Date?
|
||||
|
||||
private var currentExerciseTimer: Timer?
|
||||
public var currentExerciseTimer: Timer?
|
||||
|
||||
@Published public private(set) var currentExerciseInfo = CurrentWorkoutInfo()
|
||||
@Published var previewWorkout: Workout?
|
||||
@@ -48,346 +49,9 @@ class BridgeModule: NSObject, ObservableObject {
|
||||
private var isWatchConnected = false
|
||||
// workoutEndDate fills out WatchPackageModel.workoutEndDate which
|
||||
// tells the watch app to stop the workout
|
||||
public private(set) var workoutEndDate: Date?
|
||||
@Published public private(set) var healthKitUUID: UUID?
|
||||
public var workoutEndDate: Date?
|
||||
@Published var healthKitUUID: UUID?
|
||||
@Published var isPaused = false
|
||||
|
||||
var audioPlayer: AVAudioPlayer?
|
||||
var avPlayer: AVPlayer?
|
||||
private let session: WCSession = WCSession.default
|
||||
|
||||
func start(workout: Workout) {
|
||||
currentExerciseInfo.complete = {
|
||||
self.completeWorkout()
|
||||
}
|
||||
|
||||
currentExerciseInfo.start(workout: workout)
|
||||
currentWorkoutRunTimeInSeconds = 0
|
||||
currentWorkoutRunTimer?.invalidate()
|
||||
currentWorkoutRunTimer = nil
|
||||
isPaused = false
|
||||
|
||||
if let superetExercise = currentExerciseInfo.currentExercise {
|
||||
updateCurrent(exercise: superetExercise)
|
||||
startWorkoutTimer()
|
||||
workoutStartDate = Date()
|
||||
isInWorkout = true
|
||||
|
||||
if WCSession.isSupported() {
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func goToExerciseAt(section: Int, row: Int) {
|
||||
if let superetExercise = currentExerciseInfo.goToWorkoutAt(supersetIndex: section,
|
||||
exerciseIndex: row) {
|
||||
updateCurrent(exercise: superetExercise)
|
||||
}
|
||||
}
|
||||
|
||||
var nextExerciseObject: SupersetExercise? {
|
||||
currentExerciseInfo.goToNextExercise
|
||||
}
|
||||
|
||||
func resetCurrentWorkout() {
|
||||
DispatchQueue.main.async {
|
||||
self.currentWorkoutRunTimeInSeconds = 0
|
||||
self.currentWorkoutRunTimer?.invalidate()
|
||||
self.currentWorkoutRunTimer = nil
|
||||
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
|
||||
self.currentWorkoutRunTimeInSeconds = -1
|
||||
self.currentExerciseInfo.reset()
|
||||
|
||||
self.isInWorkout = false
|
||||
self.workoutStartDate = nil
|
||||
self.workoutEndDate = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func startWorkoutTimer() {
|
||||
currentWorkoutRunTimer?.invalidate()
|
||||
currentWorkoutRunTimer = nil
|
||||
currentWorkoutRunTimer = Timer.scheduledTimer(timeInterval: 1,
|
||||
target: self,
|
||||
selector: #selector(addOneToWorkoutRunTime),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
currentWorkoutRunTimer?.fire()
|
||||
}
|
||||
|
||||
private func startExerciseTimerWith(duration: Int) {
|
||||
DispatchQueue.main.async {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
self.currentExerciseTimeLeft = duration
|
||||
self.currentExerciseTimer = Timer.scheduledTimer(timeInterval: 1,
|
||||
target: self,
|
||||
selector: #selector(self.updateCurrentExerciseTimer),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
self.currentExerciseTimer?.fire()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateCurrentExerciseTimer() {
|
||||
if currentExerciseTimeLeft > 1 {
|
||||
currentExerciseTimeLeft -= 1
|
||||
|
||||
if let currentExercise = currentExerciseInfo.allSupersetExecercise, let audioQueues = currentExercise.audioQueues {
|
||||
if let audioQueue = audioQueues.first(where: {
|
||||
$0.playAt == currentExerciseTimeLeft
|
||||
}) {
|
||||
switch audioQueue.audioType {
|
||||
|
||||
case .shortBeep:
|
||||
playBeep()
|
||||
case .finishBeep:
|
||||
playFinished()
|
||||
case .remoteURL(let url):
|
||||
playRemoteAudio(fromURL: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
sendCurrentExerciseToWatch()
|
||||
} else {
|
||||
nextExercise()
|
||||
}
|
||||
}
|
||||
|
||||
func pauseWorkout() {
|
||||
if let _ = currentExerciseTimer {
|
||||
currentExerciseTimer?.invalidate()
|
||||
currentExerciseTimer = nil
|
||||
isPaused = true
|
||||
} else {
|
||||
isPaused = false
|
||||
startExerciseTimerWith(duration: currentExerciseTimeLeft)
|
||||
}
|
||||
}
|
||||
|
||||
func nextExercise() {
|
||||
if let nextSupersetExercise = currentExerciseInfo.goToNextExercise {
|
||||
updateCurrent(exercise: nextSupersetExercise)
|
||||
} else {
|
||||
completeWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
func previousExercise() {
|
||||
if let nextSupersetExercise = currentExerciseInfo.previousExercise {
|
||||
updateCurrent(exercise: nextSupersetExercise)
|
||||
} else {
|
||||
completeWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
func restartExercise() {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise {
|
||||
updateCurrent(exercise: currentExercise)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func addOneToWorkoutRunTime() {
|
||||
currentWorkoutRunTimeInSeconds += 1
|
||||
}
|
||||
|
||||
func updateCurrent(exercise: SupersetExercise) {
|
||||
DispatchQueue.main.async {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
|
||||
if let duration = exercise.duration, duration > 0 {
|
||||
self.startExerciseTimerWith(duration: duration)
|
||||
}
|
||||
self.sendCurrentExerciseToWatch()
|
||||
}
|
||||
}
|
||||
|
||||
func completeWorkout() {
|
||||
self.currentExerciseTimer?.invalidate()
|
||||
self.currentExerciseTimer = nil
|
||||
self.isInWorkout = false
|
||||
|
||||
workoutEndDate = Date()
|
||||
if let completedWorkout = completedWorkout {
|
||||
completedWorkout()
|
||||
self.completedWorkout = nil
|
||||
}
|
||||
}
|
||||
|
||||
func playRemoteAudio(fromURL url: URL) {
|
||||
#if os(iOS)
|
||||
let playerItem = AVPlayerItem(url: url)
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
avPlayer = AVPlayer(playerItem: playerItem)
|
||||
avPlayer?.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func playBeep() {
|
||||
#if os(iOS)
|
||||
if let path = Bundle.main.path(forResource: "short_beep", ofType: "m4a") {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
||||
audioPlayer?.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func playFinished() {
|
||||
#if os(iOS)
|
||||
if let path = Bundle.main.path(forResource: "long_beep", ofType: "m4a") {
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.mixWithOthers])
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
|
||||
audioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
||||
audioPlayer?.play()
|
||||
} catch {
|
||||
print("ERROR")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension BridgeModule: WCSessionDelegate {
|
||||
func sendResetToWatch() {
|
||||
let watchModel = PhoneToWatchActions.reset
|
||||
let data = try! JSONEncoder().encode(watchModel)
|
||||
|
||||
// user transferUserInfo b/c its guranteed to reach
|
||||
// and end the workout
|
||||
self.session.transferUserInfo(["package": data])
|
||||
}
|
||||
|
||||
func sendWorkoutCompleteToWatch() {
|
||||
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() {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise,
|
||||
let duration = currentExercise.duration ,
|
||||
duration > 0 {
|
||||
let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentTimeLeft: currentExerciseTimeLeft, workoutStartDate: workoutStartDate ?? Date())
|
||||
let model = PhoneToWatchActions.inExercise(watchModel)
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
send(data)
|
||||
} else {
|
||||
if let currentExercise = currentExerciseInfo.currentExercise,
|
||||
let reps = currentExercise.reps,
|
||||
reps > 0 {
|
||||
|
||||
// if not a timer we need to set the watch display with number of reps
|
||||
// if timer it will set when timer updates
|
||||
let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentTimeLeft: reps, workoutStartDate: self.workoutStartDate ?? Date())
|
||||
let model = PhoneToWatchActions.inExercise(watchModel)
|
||||
let data = try! JSONEncoder().encode(model)
|
||||
self.send(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
|
||||
if let model = try? JSONDecoder().decode(WatchActions.self, from: messageData) {
|
||||
switch model {
|
||||
case .nextExercise:
|
||||
nextExercise()
|
||||
playFinished()
|
||||
case .workoutComplete(let data):
|
||||
DispatchQueue.main.async {
|
||||
let model = try! JSONDecoder().decode(WatchFinishWorkoutModel.self, from: data)
|
||||
self.healthKitUUID = model.healthKitUUID
|
||||
}
|
||||
case .restartExercise:
|
||||
restartExercise()
|
||||
case .previousExercise:
|
||||
previousExercise()
|
||||
case .stopWorkout:
|
||||
completeWorkout()
|
||||
case .pauseWorkout:
|
||||
pauseWorkout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func session(_ session: WCSession,
|
||||
activationDidCompleteWith activationState: WCSessionActivationState,
|
||||
error: Error?) {
|
||||
switch activationState {
|
||||
case .notActivated:
|
||||
print("notActivated")
|
||||
case .inactive:
|
||||
print("inactive")
|
||||
case .activated:
|
||||
print("activated")
|
||||
#if os(iOS)
|
||||
let workoutConfiguration = HKWorkoutConfiguration()
|
||||
workoutConfiguration.activityType = .functionalStrengthTraining
|
||||
workoutConfiguration.locationType = .indoor
|
||||
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)
|
||||
func sessionDidBecomeInactive(_ session: WCSession) {
|
||||
|
||||
}
|
||||
|
||||
func sessionDidDeactivate(_ session: WCSession) {
|
||||
session.activate()
|
||||
}
|
||||
#endif
|
||||
func send(_ data: Data) {
|
||||
guard session.activationState == .activated else {
|
||||
return
|
||||
}
|
||||
#if os(iOS)
|
||||
guard session.isWatchAppInstalled else {
|
||||
return
|
||||
}
|
||||
#else
|
||||
guard session.isCompanionAppInstalled else {
|
||||
return
|
||||
}
|
||||
#endif
|
||||
session.sendMessageData(data, replyHandler: nil) { error in
|
||||
print("Cannot send message: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
let session: WCSession = WCSession.default
|
||||
}
|
||||
|
||||
@@ -172,6 +172,14 @@ struct WorkoutDetailView: View {
|
||||
avPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
bridgeModule.completedWorkout = {
|
||||
if let workoutData = createWorkoutData() {
|
||||
workoutComplete = .completedWorkout(workoutData)
|
||||
} else {
|
||||
bridgeModule.resetCurrentWorkout()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(
|
||||
for: UIScene.willEnterForegroundNotification)) { _ in
|
||||
@@ -194,14 +202,6 @@ struct WorkoutDetailView: View {
|
||||
}
|
||||
|
||||
func startWorkout(workout: Workout) {
|
||||
bridgeModule.completedWorkout = {
|
||||
if let workoutData = createWorkoutData() {
|
||||
workoutComplete = .completedWorkout(workoutData)
|
||||
} else {
|
||||
bridgeModule.resetCurrentWorkout()
|
||||
}
|
||||
}
|
||||
|
||||
bridgeModule.start(workout: workout)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ extension WatchMainViewModel {
|
||||
}
|
||||
isInWorkout = true
|
||||
} else {
|
||||
print("didn not init workout")
|
||||
print("did not init workout")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,9 +87,6 @@ class WatchMainViewModel: NSObject, ObservableObject {
|
||||
DispatchQueue.main.async {
|
||||
switch model {
|
||||
case .inExercise(let data):
|
||||
if self.isInWorkout == false {
|
||||
self.startWorkout()
|
||||
}
|
||||
self.watchPackageModel = data
|
||||
case .reset:
|
||||
self.isInWorkout = false
|
||||
@@ -99,6 +96,11 @@ class WatchMainViewModel: NSObject, ObservableObject {
|
||||
self.isInWorkout = false
|
||||
self.watchPackageModel = WatchMainViewModel.defualtPackageModle
|
||||
self.stopWorkout(sendDetails: true)
|
||||
case .startWorkout:
|
||||
if self.isInWorkout {
|
||||
self.stopWorkout(sendDetails: false)
|
||||
}
|
||||
self.startWorkout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user