0

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()
        }
    }
}
Cheolhyun
  • 169
  • 1
  • 7
GarySabo
  • 5,806
  • 5
  • 49
  • 124

0 Answers0