split files up, fix complete workout screen not showing if remove workout details screen

This commit is contained in:
Trey t
2024-06-21 10:41:37 -05:00
parent c101da4a4d
commit 7ce996e451
11 changed files with 441 additions and 358 deletions

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View 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
}
}

View 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()
}
}
}

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

View 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
}
}
}

View File

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

View File

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

View File

@@ -52,7 +52,7 @@ extension WatchMainViewModel {
}
isInWorkout = true
} else {
print("didn not init workout")
print("did not init workout")
}
}

View File

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