A new iOS widget extension shows the active exercise, its phase, and the work/rest countdown on the lock screen and in the Dynamic Island, driven by the run flow's live frames so locking the phone mid-set keeps the timer. The activity is seeded on open, refreshed on every page settle, dismissed when the flow is left, and cleared on next launch if stranded. Unifies the build-info stamping across all targets via a YAML anchor. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
83 lines
3.3 KiB
Swift
83 lines
3.3 KiB
Swift
//
|
|
// 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"
|
|
}
|
|
}
|
|
}
|
|
}
|