// // WorkoutActivityAttributes.swift // Workouts (shared: iOS app + Workouts Widget) // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import ActivityKit import Foundation /// The ActivityKit attributes backing the active-workout Live Activity (lock screen + /// Dynamic Island). This file is compiled into **both** the iOS app (which starts / updates / /// ends the activity via `WorkoutLiveActivityController`) and the `Workouts Widget` extension /// (which renders it). It intentionally depends on nothing but ActivityKit + Foundation, so it /// can live in the widget target without pulling in the app's model/sync layer. /// /// The whole design is **push-free**: every content state carries wall-clock anchors /// (`phaseStart` / `phaseEnd`), so the widget renders live countdowns with /// `Text(timerInterval:)` / `ProgressView(timerInterval:)` and never needs a server or a /// background update to keep ticking — the same wall-clock-anchored trick the in-app run flow /// and the watch mirror already use. struct WorkoutActivityAttributes: ActivityAttributes { /// The dynamic part of the activity — mirrors one page of the Ready → Work → Rest → Finish /// run flow. Refreshed by the app on every phase / set transition while it's foregrounded. struct ContentState: Codable, Hashable, Sendable { /// Which page of the run flow the driving device is on. enum Phase: String, Codable, Hashable, Sendable { case ready, work, rest, done } var exerciseName: String var phase: Phase /// 0-based set this frame pertains to, and the planned total. var setIndex: Int var setCount: Int /// Footer line under the timer, e.g. "8 reps" or "30 sec". var detail: String /// Wall-clock anchor for the phase's timer. A count-up work set counts up from here; a /// count-down phase (rest / timed work / auto-Done) runs from here to `phaseEnd`. var phaseStart: Date /// End anchor for a count-down phase; `nil` for a rep-based work set (which counts up) /// or the Ready lead-in (which shows no timer). var phaseEnd: Date? // MARK: Derived (shared by every widget presentation) /// 1-based set number for display. var setNumber: Int { setIndex + 1 } /// A running count-down is present (rest, timed work, or the auto-Done deadline). var isCountingDown: Bool { phaseEnd != nil } /// A rep-based work set that counts up (no end anchor). var isCountingUp: Bool { phase == .work && phaseEnd == nil } /// Short phase word for the compact/expanded labels. var phaseTitle: String { switch phase { case .ready: return "Ready" case .work: return "Work" case .rest: return "Rest" case .done: return "Done" } } /// One-line status under the exercise name, e.g. "Rest · Set 2 of 4" or /// "Set 2 of 4 · 8 reps". var statusLine: String { let sets = "Set \(setNumber) of \(setCount)" switch phase { case .ready: return "Ready · \(sets)" case .work: return detail.isEmpty ? sets : "\(sets) · \(detail)" case .rest: return "Rest · \(sets)" case .done: return "Complete" } } } }