Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ extension WorkoutSessionView {
func handleRestTick(at date: Date) async {
if let activityState = WorkoutRestActivityController.shared.currentState(sessionID: liveActivitySessionID) {
if activityState.isSkipped {
viewModel.skipRest()
settings.cancelRestNotification(sessionID: liveActivitySessionID)
await WorkoutRestActivityController.shared.end(sessionID: liveActivitySessionID)
let didSkip = await viewModel.requestServerRestSkip()
if didSkip {
settings.cancelRestNotification(sessionID: liveActivitySessionID)
await WorkoutRestActivityController.shared.end(sessionID: liveActivitySessionID)
} else {
isShowingServerActionError = viewModel.serverActionErrorMessage != nil
syncRestActivity()
}
return
}
viewModel.synchronizeRestTimer(
Expand Down Expand Up @@ -71,10 +76,66 @@ extension WorkoutSessionView {
}
if let failedAction, case .recordSet = failedAction {
completeSetSideEffects()
} else if let failedAction, case .startRest = failedAction {
syncRestActivity()
} else if let failedAction, case .skipRest = failedAction {
settings.cancelRestNotification(sessionID: liveActivitySessionID)
await WorkoutRestActivityController.shared.end(sessionID: liveActivitySessionID)
} else if let failedAction, case .extendRest = failedAction {
syncRestActivity()
} else if let failedAction, case .refreshRest = failedAction {
syncRestActivity()
}
}
}

func skipCurrentRest() {
Task {
let didSkip = await viewModel.requestServerRestSkip()
guard didSkip else {
isShowingServerActionError = viewModel.serverActionErrorMessage != nil
syncRestActivity()
return
}
settings.cancelRestNotification(sessionID: liveActivitySessionID)
await WorkoutRestActivityController.shared.end(sessionID: liveActivitySessionID)
}
}

func extendCurrentRest(by seconds: Int) {
Task {
let didExtend = await viewModel.requestServerRestExtension(by: seconds)
guard didExtend else {
isShowingServerActionError = viewModel.serverActionErrorMessage != nil
syncRestActivity()
return
}
syncRestActivity()
}
}

func refreshRestOnForeground() {
guard viewModel.isServerManagedRest else { return }
if viewModel.refreshClock(at: Date()) {
syncRestActivity()
return
}
guard viewModel.isResting else { return }
Task {
let didRefresh = await viewModel.refreshActiveServerRest(at: Date())
guard didRefresh else {
isShowingServerActionError = viewModel.serverActionErrorMessage != nil
return
}
syncRestActivity()
}
}

func skipPendingRestStart() {
guard viewModel.skipPendingRestStart() else { return }
syncRestActivity()
}

func advanceToNextExercise() {
Task {
let didAdvance = await viewModel.advanceToNextExercise()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,8 @@ extension WorkoutSessionView {
nextSetIndex: nextSetIndex,
remainingSeconds: viewModel.restSecondsRemaining,
totalSeconds: viewModel.activeRestDurationSeconds,
onExtend: {
viewModel.extendRest(by: 15)
syncRestActivity()
},
onSkip: {
viewModel.skipRest()
settings.cancelRestNotification(sessionID: liveActivitySessionID)
Task { await WorkoutRestActivityController.shared.end(sessionID: liveActivitySessionID) }
},
onExtend: { extendCurrentRest(by: 15) },
onSkip: skipCurrentRest,
previousRecordError: viewModel.previousRecordError(for: exercise.id),
isLoadingPreviousRecord: viewModel.isLoadingPreviousRecord(for: exercise.id),
onRetryPreviousRecord: { Task { await loadPreviousRecord(for: exercise.id) } }
Expand All @@ -53,6 +46,8 @@ extension WorkoutSessionView {
exerciseIndex: viewModel.currentExerciseIndex,
totalExerciseCount: viewModel.plan.exercises.count,
currentSetIndex: viewModel.currentSetIndex,
canPerformPrimaryAction: viewModel.pendingRestStart == nil &&
!viewModel.isSubmittingWorkoutAction,
isResting: viewModel.isResting || viewModel.isPaused,
isPaused: viewModel.isPaused,
restSecondsRemaining: viewModel.restSecondsRemaining,
Expand Down Expand Up @@ -83,7 +78,9 @@ extension WorkoutSessionView {
},
canToggleSet: { setIndex in
viewModel.serverWorkoutID == nil ||
(setIndex == viewModel.currentSetIndex && !viewModel.isSubmittingWorkoutAction)
(viewModel.pendingRestStart == nil &&
setIndex == viewModel.currentSetIndex &&
!viewModel.isSubmittingWorkoutAction)
},
onAdjustWeight: viewModel.adjustCurrentWeight,
onInputWeight: {
Expand All @@ -92,10 +89,7 @@ extension WorkoutSessionView {
},
onAdjustRepetitions: viewModel.adjustCurrentRepetitions,
onSetDone: { completeCurrentSet(at: Date()) },
onSkipRest: {
viewModel.skipRest()
syncRestActivity()
},
onSkipRest: skipCurrentRest,
onNextExercise: advanceToNextExercise,
onFinish: { isConfirmingFinish = true },
previousRecordError: viewModel.previousRecordError(for: exercise.id),
Expand Down
11 changes: 10 additions & 1 deletion GYMUP/Sources/Features/WorkoutSession/WorkoutSessionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import UIKit

struct WorkoutSessionView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.scenePhase) var scenePhase
@State var viewModel: WorkoutSessionViewModel
@State var settings = WorkoutSettings.shared
@State var didSaveHistoryRecord = false
Expand Down Expand Up @@ -161,6 +162,10 @@ struct WorkoutSessionView: View {
.onReceive(clock) { date in
Task { @MainActor in await handleRestTick(at: date) }
}
.onChange(of: scenePhase) { _, newPhase in
guard newPhase == .active else { return }
refreshRestOnForeground()
}
.alert(
isShowingServerActionError ? "서버 요청을 처리하지 못했어요" : "휴식 시간이 끝났어요",
isPresented: Binding(
Expand All @@ -175,7 +180,11 @@ struct WorkoutSessionView: View {
) {
if isShowingServerActionError {
Button("재시도") { retryFailedServerAction() }
Button("취소", role: .cancel) { viewModel.dismissServerActionError() }
if viewModel.failedServerAction == .startRest {
Button("휴식 건너뛰기", role: .cancel) { skipPendingRestStart() }
} else {
Button("취소", role: .cancel) { viewModel.dismissServerActionError() }
}
} else {
Button("다음 세트 진행", role: .cancel) {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ extension WorkoutSessionViewModel {
@discardableResult
func advanceToNextExercise() async -> Bool {
guard let serverWorkoutID else { return moveToNextExerciseLocally() }
guard pendingRestStart == nil else { return false }
guard hasStarted, !isPaused, !isFinished,
let currentExercise,
plan.exercises.indices.contains(currentExerciseIndex + 1) else { return false }
Expand Down Expand Up @@ -94,6 +95,14 @@ extension WorkoutSessionViewModel {
return await completeSet(exerciseIndex: exerciseIndex, setIndex: setIndex)
case .advanceExercise:
return await advanceToNextExercise()
case .startRest:
return await reconcilePendingRestStart()
case .skipRest:
return await requestServerRestSkip()
case let .extendRest(seconds):
return await requestServerRestExtension(by: seconds)
case .refreshRest:
return await refreshActiveServerRest(at: Date())
}
}

Expand All @@ -103,6 +112,7 @@ extension WorkoutSessionViewModel {

private func canCompleteSet(exerciseIndex: Int, setIndex: Int) -> Bool {
guard hasStarted, !isPaused, !isFinished,
pendingRestStart == nil,
plan.exercises.indices.contains(exerciseIndex),
plan.exercises[exerciseIndex].sets.indices.contains(setIndex) else { return false }
return !plan.exercises[exerciseIndex].sets[setIndex].isCompleted
Expand Down Expand Up @@ -139,9 +149,16 @@ extension WorkoutSessionViewModel {
idempotencyKey: pendingSetSubmission.idempotencyKey
)
try validateCreatedSet(response, workoutID: workoutID, exercise: exercise, setIndex: setIndex)
markSetCompleted(exerciseIndex: exerciseIndex, setIndex: setIndex, at: date)
markSetCompleted(exerciseIndex: exerciseIndex, setIndex: setIndex, at: date, startsLocalRest: false)
self.pendingSetSubmission = nil
return true
let hasMoreSets = exercise.sets.dropFirst(setIndex + 1).contains { !$0.isCompleted }
let restDuration = min(max(exercise.restDurationSeconds, 0), 600)
guard hasMoreSets, restDuration > 0 else { return true }
pendingRestStart = PendingWorkoutRestStart(
workoutID: workoutID,
request: StartRestRequest(setId: response.setId, durationSeconds: restDuration)
)
return await startPendingRest()
} catch {
failServerAction(.recordSet(exerciseIndex: exerciseIndex, setIndex: setIndex), error: error)
return false
Expand Down Expand Up @@ -184,7 +201,8 @@ extension WorkoutSessionViewModel {
) throws {
guard response.workoutId == workoutID,
response.exerciseId == exercise.id,
response.setIndex == setIndex + 1 else {
response.setIndex == setIndex + 1,
!response.setId.isEmpty else {
throw APIClientError.invalidResponse
}
}
Expand Down Expand Up @@ -236,6 +254,10 @@ extension WorkoutSessionViewModel {
enum FailedWorkoutServerAction: Equatable {
case recordSet(exerciseIndex: Int, setIndex: Int)
case advanceExercise
case startRest
case skipRest
case extendRest(seconds: Int)
case refreshRest
}

struct PendingWorkoutSetSubmission {
Expand All @@ -244,3 +266,8 @@ struct PendingWorkoutSetSubmission {
let request: CreateWorkoutSetRequest
let idempotencyKey: String
}

struct PendingWorkoutRestStart: Equatable {
let workoutID: String
let request: StartRestRequest
}
Loading
Loading