我试图通过将 heartRateVariabilitySDNN 添加为来自 Apple 的 SpeedySloth 示例中的谓词的统计数据来探测它,但是我从来没有得到这个变量的任何统计数据。
我知道 Breathe 应用程序会将 HRV 数据返回到 Health,所以我尝试将锻炼配置更改为“身心”、“瑜伽”等,但无济于事(室内/室外也是如此)。
根据我的阅读,我理解这是因为我们无法在锻炼期间预测 HRV - 即在撰写本文时我们无法从 Apple Watch 获得实时 HRV 测量值?我应该提到这是一个独立的手表应用程序。其他应用程序(例如 Zendo)能够在超出锻炼状态时显示 HRV,所以我想在锻炼期间给用户反馈我们不能使用 HRV;但必须查看其他指标?
在此先感谢,这里是代码:
锻炼管理器:
import Foundation
import HealthKit
import Combine
class WorkoutManager: NSObject, ObservableObject {
/// - Tag: DeclareSessionBuilder
let healthStore = HKHealthStore()
var session: HKWorkoutSession!
var builder: HKLiveWorkoutBuilder!
// Publish the following:
// - heartrate
// - active calories
// - distance moved
// - elapsed time
/// - Tag: Publishers
@Published var heartrate: Double = 0
@Published var activeCalories: Double = 0
@Published var distance: Double = 0
@Published var elapsedSeconds: Int = 0
@Published var HRV: Double = 0
// The app's workout state.
var running: Bool = false
/// - Tag: TimerSetup
// The cancellable holds the timer publisher.
var start: Date = Date()
var cancellable: Cancellable?
var accumulatedTime: Int = 0
// Set up and start the timer.
func setUpTimer() {
start = Date()
cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
.autoconnect()
.sink { [weak self] _ in
guard let self = self else { return }
self.elapsedSeconds = self.incrementElapsedTime()
}
}
// Calculate the elapsed time.
func incrementElapsedTime() -> Int {
let runningTime: Int = Int(-1 * (self.start.timeIntervalSinceNow))
return self.accumulatedTime + runningTime
}
// Request authorization to access HealthKit.
func requestAuthorization() {
// Requesting authorization.
/// - Tag: RequestAuthorization
// The quantity type to write to the health store.
let typesToShare: Set = [
HKQuantityType.workoutType()
]
// The quantity types to read from the health store.
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .heartRate)!,
HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
]
// Request authorization for those quantity types.
healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in
// Handle error.
}
}
// Provide the workout configuration.
func workoutConfiguration() -> HKWorkoutConfiguration {
/// - Tag: WorkoutConfiguration
let configuration = HKWorkoutConfiguration()
configuration.activityType = .mindAndBody
configuration.locationType = .indoor
return configuration
}
// Start the workout.
func startWorkout() {
// Start the timer.
setUpTimer()
self.running = true
// Create the session and obtain the workout builder.
/// - Tag: CreateWorkout
do {
session = try HKWorkoutSession(healthStore: healthStore, configuration: self.workoutConfiguration())
builder = session.associatedWorkoutBuilder()
} catch {
// Handle any exceptions.
print("Exception!")
return
}
// Setup session and builder.
session.delegate = self
builder.delegate = self
// Set the workout builder's data source.
/// - Tag: SetDataSource
builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
workoutConfiguration: workoutConfiguration())
// Start the workout session and begin data collection.
/// - Tag: StartSession
session.startActivity(with: Date())
builder.beginCollection(withStart: Date()) { (success, error) in
// The workout has started.
}
}
// MARK: - State Control
func togglePause() {
// If you have a timer, then the workout is in progress, so pause it.
if running == true {
self.pauseWorkout()
} else {// if session.state == .paused { // Otherwise, resume the workout.
resumeWorkout()
}
}
func pauseWorkout() {
// Pause the workout.
session.pause()
// Stop the timer.
cancellable?.cancel()
// Save the elapsed time.
accumulatedTime = elapsedSeconds
running = false
}
func resumeWorkout() {
// Resume the workout.
session.resume()
// Start the timer.
setUpTimer()
running = true
}
func endWorkout() {
// End the workout session.
session.end()
cancellable?.cancel()
}
func resetWorkout() {
// Reset the published values.
DispatchQueue.main.async {
self.elapsedSeconds = 0
self.activeCalories = 0
self.heartrate = 0
self.distance = 0
self.HRV = 0
}
}
// MARK: - Update the UI
// Update the published values.
func updateForStatistics(_ statistics: HKStatistics?) {
guard let statistics = statistics else { return }
DispatchQueue.main.async {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
/// - Tag: SetLabel
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
self.heartrate = roundedValue
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
let energyUnit = HKUnit.kilocalorie()
let value = statistics.sumQuantity()?.doubleValue(for: energyUnit)
self.activeCalories = Double( round( 1 * value! ) / 1 )
return
case HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning):
let meterUnit = HKUnit.meter()
let value = statistics.sumQuantity()?.doubleValue(for: meterUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
self.distance = roundedValue
return
case HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN):
let HRVUnit = HKUnit(from: "ms")
let value = statistics.mostRecentQuantity()?.doubleValue(for: HRVUnit)
let roundedValue = Double( round( 1 * value! ) / 1)
self.HRV = roundedValue
default:
return
}
}
}
}
// MARK: - HKWorkoutSessionDelegate
extension WorkoutManager: HKWorkoutSessionDelegate {
func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState,
from fromState: HKWorkoutSessionState, date: Date) {
// Wait for the session to transition states before ending the builder.
/// - Tag: SaveWorkout
if toState == .ended {
print("The workout has now ended.")
builder.endCollection(withEnd: Date()) { (success, error) in
self.builder.finishWorkout { (workout, error) in
// Optionally display a workout summary to the user.
self.resetWorkout()
}
}
}
}
func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
}
}
// MARK: - HKLiveWorkoutBuilderDelegate
extension WorkoutManager: HKLiveWorkoutBuilderDelegate {
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
}
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return // Nothing to do.
}
/// - Tag: GetStatistics
let statistics = workoutBuilder.statistics(for: quantityType)
// Update the published values.
updateForStatistics(statistics)
}
}
}
和锻炼视图:
import SwiftUI
struct WorkoutView: View {
@EnvironmentObject var workoutSession: WorkoutManager
var body: some View {
VStack(alignment: .leading) {
// The workout elapsed time.
Text("\(elapsedTimeString(elapsed: secondsToHoursMinutesSeconds(seconds: workoutSession.elapsedSeconds)))").frame(alignment: .leading)
.font(Font.system(size: 26, weight: .semibold, design: .default).monospacedDigit())
// The active calories burned.
Text("\(workoutSession.activeCalories, specifier: "%.1f") cal")
.font(Font.system(size: 26, weight: .regular, design: .default).monospacedDigit())
.frame(alignment: .leading)
// The current heartrate.
Text("\(workoutSession.heartrate, specifier: "%.1f") BPM")
.font(Font.system(size: 26, weight: .regular, design: .default).monospacedDigit())
// The distance traveled.
Text("\(workoutSession.distance, specifier: "%.1f") m")
.font(Font.system(size: 26, weight: .regular, design: .default).monospacedDigit())
Spacer().frame(width: 1, height: 8, alignment: .leading)
// The HRV
Text("\(workoutSession.HRV, specifier: "%.0f") ms")
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .leading)
}
// Convert the seconds into seconds, minutes, hours.
func secondsToHoursMinutesSeconds (seconds: Int) -> (Int, Int, Int) {
return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
}
// Convert the seconds, minutes, hours into a string.
func elapsedTimeString(elapsed: (h: Int, m: Int, s: Int)) -> String {
return String(format: "%d:%02d:%02d", elapsed.h, elapsed.m, elapsed.s)
}
}
struct WorkoutView_Previews: PreviewProvider {
static var previews: some View {
WorkoutView().environmentObject(WorkoutManager())
}
}