I'm following this Apple Sample Code almost exactly except for some subtle changes in the WorkoutManager class. However, when testing on the simulator in Always On mode, Apple's app switches its context.cadence
from .live
to .seconds
, whereas my app switches its context.cadence
to .minutes
. As a result my app doesn't update as often as Apple's app. I can't figure out where I am doing anything differently or how to set mine to .seconds
as well - which I thought was the default?
My code:
struct LiveWorkoutTabView: View {
@State var tabIndex:Int = 1
@EnvironmentObject var workoutManager:
var body: some View {
if workoutManager.workoutIsActive {
TimelineView(MetricsTimelineSchedule(from: workoutManager.startDate ?? Date(),
isPaused: workoutManager.session?.state == .paused)) { context in
TabView(selection: $tabIndex) {
WorkoutControlsView(liveWorkoutTabViewIndex: $tabIndex).environmentObject(workoutManager)
.tabItem { Group{
}}.tag(0)
LiveWorkoutHeartView().environmentObject(workoutManager)
.tabItem { Group{
}}.tag(1)
}
.onChange(of: context.cadence, perform: { newValue in
print("cadence = \(newValue)")
})
.navigationTitle {
HStack {
ElapsedTimeView(elapsedTime: workoutManager.builder?.elapsedTime(at: context.date) ?? 0, showSubseconds: context.cadence == .live)
Spacer()
}
}
}
}
}
}
struct WorkoutTabView_Previews: PreviewProvider {
static var previews: some View {
LiveWorkoutTabView().environmentObject(WorkoutManager())
}
}
//For making the timer only show seconds when Always On display is on, Code from: https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/build_a_workout_app_for_apple_watch
struct ElapsedTimeView: View {
var elapsedTime: TimeInterval = 0
var showSubseconds: Bool = true
@State private var timeFormatter = ElapsedTimeFormatter()
var body: some View {
Text(NSNumber(value: elapsedTime), formatter: timeFormatter)
.font(.body)
.foregroundColor(.white)
.monospacedDigit()
.onChange(of: showSubseconds) {
timeFormatter.showSubseconds = $0
}
}
}
class ElapsedTimeFormatter: Formatter {
let componentsFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = .pad
return formatter
}()
var showSubseconds = true
override func string(for value: Any?) -> String? {
guard let time = value as? TimeInterval else {
return nil
}
guard let formattedString = componentsFormatter.string(from: time) else {
return nil
}
if showSubseconds {
let hundredths = Int((time.truncatingRemainder(dividingBy: 1)) * 100)
let decimalSeparator = Locale.current.decimalSeparator ?? "."
return String(format: "%@%@%0.2d", formattedString, decimalSeparator, hundredths)
}
return formattedString
}
}
private struct MetricsTimelineSchedule: TimelineSchedule {
var startDate: Date
var isPaused: Bool
init(from startDate: Date, isPaused: Bool) {
self.startDate = startDate
self.isPaused = isPaused
}
func entries(from startDate: Date, mode: TimelineScheduleMode) -> AnyIterator<Date> {
var baseSchedule = PeriodicTimelineSchedule(from: self.startDate,
by: (mode == .lowFrequency ? 1.0 : 1.0 / 30.0))
.entries(from: startDate, mode: mode)
return AnyIterator<Date> {
guard !isPaused else { return nil }
return baseSchedule.next()
}
}
}
Apple's Code:
struct MetricsView: View {
@EnvironmentObject var workoutManager: WorkoutManager
var body: some View {
TimelineView(MetricsTimelineSchedule(from: workoutManager.builder?.startDate ?? Date(),
isPaused: workoutManager.session?.state == .paused)) { context in
VStack(alignment: .leading) {
ElapsedTimeView(elapsedTime: workoutManager.builder?.elapsedTime(at: context.date) ?? 0, showSubseconds: context.cadence == .live)
.foregroundStyle(.yellow)
Text(Measurement(value: workoutManager.activeEnergy, unit: UnitEnergy.kilocalories)
.formatted(.measurement(width: .abbreviated, usage: .workout, numberFormatStyle: .number.precision(.fractionLength(0)))))
Text(workoutManager.heartRate.formatted(.number.precision(.fractionLength(0))) + " bpm")
Text(Measurement(value: workoutManager.distance, unit: UnitLength.meters).formatted(.measurement(width: .abbreviated, usage: .road)))
}
.font(.system(.title, design: .rounded).monospacedDigit().lowercaseSmallCaps())
.frame(maxWidth: .infinity, alignment: .leading)
.ignoresSafeArea(edges: .bottom)
.scenePadding()
.onChange(of: context.cadence, perform: { newValue in
print("cadence = \(newValue)")
})
}
}
}
struct MetricsView_Previews: PreviewProvider {
static var previews: some View {
MetricsView().environmentObject(WorkoutManager())
}
}
private struct MetricsTimelineSchedule: TimelineSchedule {
var startDate: Date
var isPaused: Bool
init(from startDate: Date, isPaused: Bool) {
self.startDate = startDate
self.isPaused = isPaused
}
func entries(from startDate: Date, mode: TimelineScheduleMode) -> AnyIterator<Date> {
var baseSchedule = PeriodicTimelineSchedule(from: self.startDate,
by: (mode == .lowFrequency ? 1.0 : 1.0 / 30.0))
.entries(from: startDate, mode: mode)
return AnyIterator<Date> {
guard !isPaused else { return nil }
return baseSchedule.next()
}
}
}