From 67e2bdc8c310551d47988b34f1a2bb8c9e86b424 Mon Sep 17 00:00:00 2001 From: Millian Lamiaux Date: Sun, 17 May 2026 00:43:01 +0200 Subject: [PATCH] Redesign workout live activity with circular timer ring, phase icons, and smoother updates - Add CountdownRing with real-time arc progress on lock screen - Replace generic dots with phase-specific SF Symbols (flame, snowflake, etc.) - Remove horizontal progress bar in favor of round counter text - Increase Dynamic Island expanded font sizes for better visibility - Increase live activity sync frequency from 5s to 1s for smoother arc updates - Add pause/resume button via TogglePauseIntent AppIntent - Remove AlertConfiguration to silence notification sounds on updates --- .../Models/WorkoutActivityAttributes.swift | 1 + .../TabataGo/ViewModels/PlayerViewModel.swift | 8 +- .../TabataGoWidget/WorkoutLiveActivity.swift | 396 ++++++++++++------ 3 files changed, 271 insertions(+), 134 deletions(-) diff --git a/tabatago-swift/TabataGo/Models/WorkoutActivityAttributes.swift b/tabatago-swift/TabataGo/Models/WorkoutActivityAttributes.swift index bb28a20..dddd5f8 100644 --- a/tabatago-swift/TabataGo/Models/WorkoutActivityAttributes.swift +++ b/tabatago-swift/TabataGo/Models/WorkoutActivityAttributes.swift @@ -40,6 +40,7 @@ struct WorkoutActivityAttributes: ActivityAttributes { var exerciseName: String var phase: WorkoutPhase var phaseEndDate: Date + var phaseDuration: TimeInterval var roundCurrent: Int var roundTotal: Int var heartRate: Double diff --git a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift index 88fe888..dbdcb38 100644 --- a/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift +++ b/tabatago-swift/TabataGo/ViewModels/PlayerViewModel.swift @@ -365,7 +365,7 @@ final class PlayerViewModel: ObservableObject { // ─── Dynamic Island / Live Activity ───────────────────────────── - func syncActivity() { + func syncActivity(shouldAlert: Bool = false) { guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } guard isRunning else { return } @@ -377,6 +377,7 @@ final class PlayerViewModel: ObservableObject { exerciseName: currentExercise?.nameEn ?? "", phase: workoutPhase, phaseEndDate: phaseEnd, + phaseDuration: Double(totalPhaseTime), roundCurrent: currentRound, roundTotal: totalRoundsInBlock, heartRate: heartRate, @@ -393,7 +394,7 @@ final class PlayerViewModel: ObservableObject { if let existing = workoutActivity { nonisolated(unsafe) let safeExisting = existing let staleDate = Date().addingTimeInterval(isPaused ? 3600 : 120) - let alert = alertForPhase(workoutPhase) + let alert = shouldAlert ? alertForPhase(workoutPhase) : nil Task { @MainActor in if let alert { await safeExisting.update(ActivityContent(state: state, staleDate: staleDate), alertConfiguration: alert) @@ -444,6 +445,7 @@ final class PlayerViewModel: ObservableObject { exerciseName: safeActivity.content.state.exerciseName, phase: .complete, phaseEndDate: safeActivity.content.state.phaseEndDate, + phaseDuration: safeActivity.content.state.phaseDuration, roundCurrent: safeActivity.content.state.roundCurrent, roundTotal: safeActivity.content.state.roundTotal, heartRate: safeActivity.content.state.heartRate, @@ -457,7 +459,7 @@ final class PlayerViewModel: ObservableObject { private func startActivitySyncTimer() { stopActivitySyncTimer() - activitySyncTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in + activitySyncTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in Task { @MainActor [weak self] in self?.syncActivity() } diff --git a/tabatago-swift/TabataGoWidget/WorkoutLiveActivity.swift b/tabatago-swift/TabataGoWidget/WorkoutLiveActivity.swift index 352e040..3177aa9 100644 --- a/tabatago-swift/TabataGoWidget/WorkoutLiveActivity.swift +++ b/tabatago-swift/TabataGoWidget/WorkoutLiveActivity.swift @@ -1,34 +1,48 @@ import ActivityKit import WidgetKit import SwiftUI +import AppIntents -// MARK: - Decomposed Views +// MARK: - App Intents -struct PhasePill: View { - let phase: WorkoutPhase - let isPaused: Bool +struct TogglePauseIntent: AppIntent { + static let title: LocalizedStringResource = "Toggle Pause" - var body: some View { - if isPaused { - Text("PAUSED") - .font(.caption2.bold()) - .foregroundStyle(.white) - .padding(.horizontal, 5) - .padding(.vertical, 2) - .background(.white.opacity(0.15)) - .clipShape(Capsule()) - } else { - Text(phase.capitalized.uppercased()) - .font(.caption.bold()) - .foregroundStyle(phase.color) - .padding(.horizontal, 5) - .padding(.vertical, 2) - .background(phase.color.opacity(0.15)) - .clipShape(Capsule()) - } + @MainActor + func perform() async throws -> some IntentResult & OpensIntent { + let url = URL(string: "tabatago://togglePause")! + return .result(opensIntent: OpenURLIntent(url)) } } +// MARK: - Phase Icon + +struct PhaseIcon: View { + let phase: WorkoutPhase + let isPaused: Bool + let size: CGFloat + + private var iconName: String { + if isPaused { return "pause.fill" } + switch phase { + case .work: return "flame.fill" + case .rest, .interBlockRest: return "snowflake" + case .prep, .warmup: return "timer" + case .cooldown: return "heart.circle.fill" + case .complete: return "checkmark.circle.fill" + } + } + + var body: some View { + Image(systemName: iconName) + .font(.system(size: size)) + .foregroundStyle(isPaused ? .white.opacity(0.4) : phase.color) + .modifier(PulseEffect(active: phase == .work && !isPaused)) + } +} + +// MARK: - Countdown Text + struct CountdownText: View { let endDate: Date let isPaused: Bool @@ -59,6 +73,63 @@ struct CountdownText: View { } } +// MARK: - Countdown Ring (Lock Screen only) + +struct CountdownRing: View { + let endDate: Date + let phaseDuration: TimeInterval + let isPaused: Bool + let frozenSeconds: Int + let isUrgent: Bool + let phase: WorkoutPhase + let size: CGFloat + + var body: some View { + TimelineView(.periodic(from: .now, by: 0.2)) { timeline in + let lineWidth: CGFloat = 4 + let remaining = max(0, endDate.timeIntervalSince(timeline.date)) + let activeProgress = phaseDuration > 0 ? CGFloat(remaining / phaseDuration) : 1.0 + let frozenProgress = phaseDuration > 0 ? CGFloat(frozenSeconds) / CGFloat(phaseDuration) : 1.0 + let arcProgress = isPaused ? frozenProgress : activeProgress + + ZStack { + Circle() + .stroke(.white.opacity(0.08), lineWidth: lineWidth) + + Circle() + .trim(from: 0, to: arcProgress) + .stroke( + isPaused ? phase.color.opacity(0.3) : (isUrgent ? .orange : phase.color), + style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + .animation(.linear(duration: 0.2), value: arcProgress) + + if isPaused { + Text(CountdownText.formatFrozenTime(frozenSeconds)) + .font(.system(size: size * 0.32, weight: .bold).monospacedDigit()) + .foregroundStyle(.white.opacity(0.5)) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } else { + Text(timerInterval: Date()...endDate, countsDown: true) + .font(.system(size: size * 0.32, weight: .bold).monospacedDigit()) + .foregroundStyle(isUrgent ? .orange : .white) + .contentTransition(.numericText(countsDown: true)) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } + } + .frame(width: size, height: size) + } + .accessibilityLabel(isPaused + ? "Timer paused at \(CountdownText.formatFrozenTime(frozenSeconds))" + : "Countdown timer") + } +} + +// MARK: - Workout Progress Bar + struct WorkoutProgressBar: View { let progress: CGFloat let color: Color @@ -67,18 +138,20 @@ struct WorkoutProgressBar: View { var body: some View { GeometryReader { geo in ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 1, style: .continuous) + RoundedRectangle(cornerRadius: 2, style: .continuous) .fill(.white.opacity(0.08)) - RoundedRectangle(cornerRadius: 1, style: .continuous) - .fill(color.opacity(isPaused ? 0.25 : 0.6)) + RoundedRectangle(cornerRadius: 2, style: .continuous) + .fill(color.opacity(isPaused ? 0.25 : 0.7)) .frame(width: geo.size.width * progress) } - .frame(height: 2) + .frame(height: 4) } - .frame(height: 2) + .frame(height: 4) } } +// MARK: - Music Info Row + struct MusicInfoRow: View { let trackTitle: String let trackArtist: String @@ -100,7 +173,6 @@ struct MusicInfoRow: View { .font(.caption2) .lineLimit(1) .foregroundStyle(.white.opacity(isPaused ? 0.3 : 0.5)) - .accessibilityLabel(trackArtist.isEmpty ? trackTitle : "\(trackTitle) by \(trackArtist)") Spacer() @@ -125,7 +197,6 @@ struct HeartRateBadge: View { Image(systemName: "heart.fill") .font(.system(size: 8)) .foregroundStyle(.red.opacity(0.7)) - .accessibilityLabel("Heart rate") Text("\(Int(heartRate))") .font(.system(size: 10).monospacedDigit()) .foregroundStyle(.white.opacity(isPaused ? 0.3 : 0.5)) @@ -134,17 +205,40 @@ struct HeartRateBadge: View { } } -struct PhaseIndicatorDot: View { - let color: Color +// MARK: - DI Bottom Info Row + +struct DIBottomInfoRow: View { + let heartRate: Double + let trackTitle: String let isPaused: Bool - let size: CGFloat - let isPulsing: Bool var body: some View { - Circle() - .fill(isPaused ? color.opacity(0.4) : color) - .frame(width: size, height: size) - .modifier(PulseEffect(active: isPulsing)) + HStack { + if heartRate > 0 { + HStack(spacing: 3) { + Image(systemName: "heart.fill") + .font(.system(size: 10)) + .foregroundStyle(.red.opacity(0.7)) + Text("\(Int(heartRate))") + .font(.caption.monospacedDigit()) + .foregroundStyle(.white.opacity(isPaused ? 0.3 : 0.5)) + } + } + + Spacer() + + if !trackTitle.isEmpty { + HStack(spacing: 3) { + Image(systemName: "music.note") + .font(.system(size: 10)) + .foregroundStyle(.green) + Text(trackTitle) + .font(.caption) + .lineLimit(1) + .foregroundStyle(.white.opacity(isPaused ? 0.3 : 0.5)) + } + } + } } } @@ -164,69 +258,96 @@ struct WorkoutLiveActivity: Widget { switch activityFamily { case .small: - WorkoutSmallView(phase: phase, paused: paused, frozenSeconds: frozenSeconds, exerciseName: context.state.exerciseName, roundCurrent: context.state.roundCurrent, roundTotal: context.state.roundTotal, endDate: context.state.phaseEndDate) + WorkoutSmallView( + phase: phase, + paused: paused, + frozenSeconds: frozenSeconds, + exerciseName: context.state.exerciseName, + roundCurrent: context.state.roundCurrent, + roundTotal: context.state.roundTotal, + endDate: context.state.phaseEndDate + ) default: - WorkoutLockScreenView(phase: phase, paused: paused, frozenSeconds: frozenSeconds, isFullscreen: isFullscreen, isLuminanceReduced: isLuminanceReduced, isUrgent: isUrgent, isStale: context.isStale, exerciseName: context.state.exerciseName, roundCurrent: context.state.roundCurrent, roundTotal: context.state.roundTotal, endDate: context.state.phaseEndDate, heartRate: context.state.heartRate, trackTitle: context.state.trackTitle, trackArtist: context.state.trackArtist, isPlaying: context.state.isPlaying) + WorkoutLockScreenView( + phase: phase, + paused: paused, + frozenSeconds: frozenSeconds, + isFullscreen: isFullscreen, + isLuminanceReduced: isLuminanceReduced, + isUrgent: isUrgent, + isStale: context.isStale, + exerciseName: context.state.exerciseName, + roundCurrent: context.state.roundCurrent, + roundTotal: context.state.roundTotal, + endDate: context.state.phaseEndDate, + phaseDuration: context.state.phaseDuration, + heartRate: context.state.heartRate, + trackTitle: context.state.trackTitle, + trackArtist: context.state.trackArtist, + isPlaying: context.state.isPlaying + ) } } dynamicIsland: { context in let phase = context.state.phase let paused = context.state.isPaused let frozenSeconds = Int(max(0, context.state.phaseEndDate.timeIntervalSinceNow)) let isUrgent = !paused && frozenSeconds > 0 && frozenSeconds <= 5 - let isWork = phase == .work && !paused let progress = CGFloat(context.state.roundCurrent) / CGFloat(max(context.state.roundTotal, 1)) return DynamicIsland { DynamicIslandExpandedRegion(.leading) { - HStack(spacing: 4) { - PhasePill(phase: phase, isPaused: paused) - Text("\(context.state.roundCurrent)/\(context.state.roundTotal)") + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 4) { + PhaseIcon(phase: phase, isPaused: paused, size: 16) + Text(paused ? "PAUSED" : phase.capitalized.uppercased()) + .font(.subheadline.bold()) + .foregroundStyle(paused ? .white.opacity(0.5) : phase.color) + } + Text("Rd \(context.state.roundCurrent)/\(context.state.roundTotal)") .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } + .padding(.vertical, 2) .accessibilityLabel(paused ? "Paused, round \(context.state.roundCurrent) of \(context.state.roundTotal)" : "\(phase.capitalized) phase, round \(context.state.roundCurrent) of \(context.state.roundTotal)") } DynamicIslandExpandedRegion(.center) { Text(context.state.exerciseName) - .font(.caption.weight(.semibold)) + .font(.body.weight(.semibold)) .foregroundStyle(paused ? .white.opacity(0.6) : .white) .lineLimit(1) .accessibilityLabel("Exercise \(context.state.exerciseName)") } DynamicIslandExpandedRegion(.trailing) { - CountdownText(endDate: context.state.phaseEndDate, isPaused: paused, frozenSeconds: frozenSeconds, isUrgent: isUrgent, size: 20) - .frame(width: 56, alignment: .trailing) + CountdownText(endDate: context.state.phaseEndDate, isPaused: paused, frozenSeconds: frozenSeconds, isUrgent: isUrgent, size: 26) + .frame(width: 64, alignment: .trailing) } DynamicIslandExpandedRegion(.bottom) { - VStack(spacing: 6) { + VStack(spacing: 8) { WorkoutProgressBar(progress: progress, color: phase.color, isPaused: paused) - MusicInfoRow( - trackTitle: context.state.trackTitle, - trackArtist: context.state.trackArtist, - isPlaying: context.state.isPlaying, - isPaused: paused, + DIBottomInfoRow( heartRate: context.state.heartRate, - isLuminanceReduced: false + trackTitle: context.state.trackTitle, + isPaused: paused ) } - .padding(.bottom, 4) + .padding(.bottom, 8) } } compactLeading: { - PhaseIndicatorDot(color: phase.color, isPaused: paused, size: 10, isPulsing: isWork) + PhaseIcon(phase: phase, isPaused: paused, size: 14) .accessibilityLabel(paused ? "Paused" : "\(phase.capitalized) phase, workout in progress") } compactTrailing: { CountdownText(endDate: context.state.phaseEndDate, isPaused: paused, frozenSeconds: frozenSeconds, isUrgent: isUrgent, size: 12) } minimal: { - PhaseIndicatorDot(color: phase.color, isPaused: paused, size: 6, isPulsing: false) + PhaseIcon(phase: phase, isPaused: paused, size: 10) .accessibilityLabel(paused ? "Paused" : "Workout in progress") } .keylineTint(phase.color) .contentMargins(.leading, 8, for: .expanded) .contentMargins(.trailing, 8, for: .expanded) - .contentMargins(.bottom, 4, for: .expanded) + .contentMargins(.bottom, 6, for: .expanded) .widgetURL(URL(string: "tabatago://workout")!) } .supplementalActivityFamilies([.small, .medium]) @@ -247,90 +368,98 @@ struct WorkoutLockScreenView: View { let roundCurrent: Int let roundTotal: Int let endDate: Date + let phaseDuration: TimeInterval let heartRate: Double let trackTitle: String let trackArtist: String let isPlaying: Bool - var progress: CGFloat { CGFloat(roundCurrent) / CGFloat(max(roundTotal, 1)) } - var body: some View { - VStack(spacing: 0) { - HStack(alignment: .center, spacing: 12) { - RoundedRectangle(cornerRadius: 4, style: .continuous) - .fill(paused ? phase.color.opacity(0.4) : phase.color) - .frame(width: 4, height: 38) - .accessibilityHidden(true) + ZStack { + LinearGradient( + stops: [ + .init(color: .black, location: 0.4), + .init(color: phase.color.opacity(0.1), location: 1.0) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) - VStack(alignment: .leading, spacing: 2) { - Text(exerciseName) - .font(isFullscreen ? .title2 : .headline) - .foregroundStyle(.primary) - .minimumScaleFactor(0.7) - .accessibilityLabel(exerciseName) - if paused { - Text("PAUSED") - .font(.caption.bold()) - .foregroundStyle(.white.opacity(0.6)) - .accessibilityLabel("Paused") - } else { - Text(phase.capitalized) - .font(.caption.weight(.semibold)) - .foregroundStyle(phase.color) - .accessibilityLabel("\(phase.capitalized) phase") - } - if isStale { - Text("Last updated") - .font(.caption2) - .foregroundStyle(.secondary.opacity(0.6)) - } - } - .layoutPriority(1) + VStack(spacing: 0) { + HStack(alignment: .center, spacing: 14) { + CountdownRing( + endDate: endDate, + phaseDuration: phaseDuration, + isPaused: paused, + frozenSeconds: frozenSeconds, + isUrgent: isUrgent, + phase: phase, + size: isFullscreen ? 64 : 56 + ) - Spacer() + VStack(alignment: .leading, spacing: 3) { + Text(exerciseName) + .font(isFullscreen ? .title2.bold() : .headline.bold()) + .foregroundStyle(.primary) + .minimumScaleFactor(0.7) + .lineLimit(1) - VStack(alignment: .trailing, spacing: 2) { - Group { - if paused { - Text(CountdownText.formatFrozenTime(frozenSeconds)) - .font(isFullscreen ? .largeTitle.weight(.bold) : .title2.weight(.bold)) - .fontDesign(.monospaced) - .foregroundStyle(.white.opacity(0.5)) - } else { - Text(timerInterval: Date()...endDate, countsDown: true) - .font(isFullscreen ? .largeTitle.weight(.bold) : .title2.weight(.bold)) - .fontDesign(.monospaced) - .foregroundStyle(isUrgent ? .orange : .primary) - .contentTransition(.numericText(countsDown: true)) + HStack(spacing: 5) { + PhaseIcon(phase: phase, isPaused: paused, size: 10) + if paused { + Text("PAUSED") + .font(.caption.bold()) + .foregroundStyle(.white.opacity(0.6)) + } else { + Text(phase.capitalized) + .font(.caption.weight(.semibold)) + .foregroundStyle(phase.color) + } + Text("·") + .foregroundStyle(.secondary.opacity(0.4)) + Text("Round \(roundCurrent)/\(roundTotal)") + .font(.caption) + .foregroundStyle(.secondary) + } + + if isStale { + Text("Last updated") + .font(.caption2) + .foregroundStyle(.secondary.opacity(0.6)) } } - .accessibilityLabel(paused ? "Timer paused at \(CountdownText.formatFrozenTime(frozenSeconds))" : "Countdown timer") - Text("Round \(roundCurrent)/\(roundTotal)") - .font(.caption2) - .foregroundStyle(.secondary) - .accessibilityLabel("Round \(roundCurrent) of \(roundTotal)") + .layoutPriority(1) + + Spacer(minLength: 0) + + Button(intent: TogglePauseIntent()) { + Image(systemName: paused ? "play.fill" : "pause.fill") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(.white.opacity(0.8)) + .frame(width: 32, height: 32) + .background(.white.opacity(0.12)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .accessibilityLabel(paused ? "Resume workout" : "Pause workout") } + .padding(.horizontal, 16) + .padding(.top, isFullscreen ? 20 : 12) + + MusicInfoRow( + trackTitle: trackTitle, + trackArtist: trackArtist, + isPlaying: isPlaying, + isPaused: paused, + heartRate: heartRate, + isLuminanceReduced: isLuminanceReduced + ) + .padding(.horizontal, 16) + .padding(.top, 6) + .padding(.bottom, 10) } - .padding(.horizontal, 14) - .padding(.top, isFullscreen ? 20 : 12) - - WorkoutProgressBar(progress: progress, color: phase.color, isPaused: paused) - .padding(.horizontal, 14) - .padding(.top, 8) - - MusicInfoRow( - trackTitle: trackTitle, - trackArtist: trackArtist, - isPlaying: isPlaying, - isPaused: paused, - heartRate: heartRate, - isLuminanceReduced: isLuminanceReduced - ) - .padding(.horizontal, 14) - .padding(.top, 6) - .padding(.bottom, 10) } - .activityBackgroundTint(.black.opacity(0.9)) + .activityBackgroundTint(.clear) .activitySystemActionForegroundColor(.white) } } @@ -348,19 +477,16 @@ struct WorkoutSmallView: View { var body: some View { HStack(spacing: 8) { - PhaseIndicatorDot(color: phase.color, isPaused: paused, size: 8, isPulsing: false) - .accessibilityLabel(paused ? "Paused" : "\(phase.capitalized) phase") + PhaseIcon(phase: phase, isPaused: paused, size: 10) VStack(alignment: .leading, spacing: 1) { Text(exerciseName) .font(.caption.weight(.semibold)) .lineLimit(1) .foregroundStyle(paused ? Color.primary.opacity(0.6) : Color.primary) - .accessibilityLabel(exerciseName) Text(paused ? "PAUSED" : "\(roundCurrent)/\(roundTotal)") .font(.caption2.monospacedDigit()) .foregroundStyle(.secondary) - .accessibilityLabel(paused ? "Paused" : "Round \(roundCurrent) of \(roundTotal)") } Spacer() @@ -403,6 +529,7 @@ private struct PulseEffect: ViewModifier { exerciseName: "Burpees", phase: .work, phaseEndDate: .now.addingTimeInterval(20), + phaseDuration: 20, roundCurrent: 3, roundTotal: 8, heartRate: 142, @@ -420,6 +547,7 @@ private struct PulseEffect: ViewModifier { exerciseName: "Mountain Climbers", phase: .rest, phaseEndDate: .now.addingTimeInterval(8), + phaseDuration: 10, roundCurrent: 4, roundTotal: 8, heartRate: 128, @@ -437,6 +565,7 @@ private struct PulseEffect: ViewModifier { exerciseName: "Jump Squats", phase: .work, phaseEndDate: .now.addingTimeInterval(15), + phaseDuration: 20, roundCurrent: 5, roundTotal: 8, heartRate: 135, @@ -454,6 +583,7 @@ private struct PulseEffect: ViewModifier { exerciseName: "Workout Complete", phase: .complete, phaseEndDate: .now, + phaseDuration: 0, roundCurrent: 8, roundTotal: 8, heartRate: 110, @@ -471,6 +601,7 @@ private struct PulseEffect: ViewModifier { exerciseName: "Burpees", phase: .work, phaseEndDate: .now.addingTimeInterval(18), + phaseDuration: 20, roundCurrent: 3, roundTotal: 8, heartRate: 145, @@ -483,6 +614,7 @@ private struct PulseEffect: ViewModifier { exerciseName: "High Knees", phase: .rest, phaseEndDate: .now.addingTimeInterval(6), + phaseDuration: 10, roundCurrent: 6, roundTotal: 8, heartRate: 120, @@ -500,6 +632,7 @@ private struct PulseEffect: ViewModifier { exerciseName: "Burpees", phase: .work, phaseEndDate: .now.addingTimeInterval(18), + phaseDuration: 20, roundCurrent: 3, roundTotal: 8, heartRate: 0, @@ -517,6 +650,7 @@ private struct PulseEffect: ViewModifier { exerciseName: "Burpees", phase: .work, phaseEndDate: .now.addingTimeInterval(18), + phaseDuration: 20, roundCurrent: 3, roundTotal: 8, heartRate: 0,