i am trying to fetch the the user last sleep cycle, store the sleep cycle within all the properties i am interested (Sleep Phases, Sleep Cycle Duration, average heart rate) and then visualize it. i created a HealthStore() class in which there are all the logic to fetch calculate and save the sleep cycle. when i fetch data nothing is stored, nothing is fetched and nothing is visualized.
this is my HealthStore() class:
import HealthKit
protocol HealthStoreDelegate: AnyObject {
func latestSleepCycleDataUpdated()
}
class HealthStore: ObservableObject {
weak var delegate: HealthStoreDelegate?
@Published var sleepCycles: [SleepCycleData] {
didSet {
saveSleepCycles()
delegate?.latestSleepCycleDataUpdated()
}
}
@Published var latestCycleData: SleepCycleData? {
didSet {
saveLatestCycleData()
delegate?.latestSleepCycleDataUpdated()
}
}
private let healthStore = HKHealthStore()
private let sleepCyclesKey = "sleepCycles"
private let latestCycleDataKey = "latestCycleData"
init() {
sleepCycles = []
latestCycleData = nil
loadSleepCycles()
loadLatestCycleData()
requestHealthKitAuthorization()
}
func requestHealthKitAuthorization() {
guard HKHealthStore.isHealthDataAvailable() else {
print("HealthKit is not available on this device.")
return
}
let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
let typesToShare: Set<HKSampleType> = [] // No data to share in this example
let typesToRead: Set<HKObjectType> = [heartRateType, sleepType]
healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { [weak self] success, error in
if success {
print("Authorization request succeeded.")
self?.fetchSleepCycles()
// You can now access heart rate and sleep cycle data
} else if let error = error {
print("Authorization request failed: \(error.localizedDescription)")
} else {
print("Unknown authorization error.")
}
}
}
func fetchSleepCycles() {
let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
let calendar = Calendar.current
let now = Date()
let twentyFourHoursAgo = calendar.date(byAdding: .hour, value: -24, to: .now)!
let predicate = HKQuery.predicateForSamples(withStart: twentyFourHoursAgo, end: .now, options: .strictStartDate)
let query = HKAnchoredObjectQuery(
type: sleepType,
predicate: predicate,
anchor: nil,
limit: HKObjectQueryNoLimit
) { [weak self] query, samples, deletedObjects, anchor, error in
guard let self = self, let sleepSamples = samples as? [HKCategorySample] else { return }
var sleepPhaseDurations: [HKCategoryValueSleepAnalysis: TimeInterval] = [:]
var sleepCycleHeartRates: [HKCategoryValueSleepAnalysis: [Double]] = [:]
let dispatchGroup = DispatchGroup() // Create a dispatch group
for sample in sleepSamples {
let value = HKCategoryValueSleepAnalysis(rawValue: sample.value)!
let duration = sample.endDate.timeIntervalSince(sample.startDate)
sleepPhaseDurations[value, default: 0] += duration
let heartRatePredicate = HKQuery.predicateForSamples(
withStart: sample.startDate,
end: sample.endDate,
options: []
)
dispatchGroup.enter() // Enter the dispatch group
let heartRateQuery = HKSampleQuery(
sampleType: heartRateType,
predicate: heartRatePredicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil
) { query, results, error in
defer {
dispatchGroup.leave() // Leave the dispatch group when the query is completed
}
if let heartRateSamples = results as? [HKQuantitySample] {
let heartRates = heartRateSamples.map { $0.quantity.doubleValue(for: HKUnit.count().unitDivided(by: .minute())) }
sleepCycleHeartRates[value, default: []].append(contentsOf: heartRates)
}
}
self.healthStore.execute(heartRateQuery)
}
dispatchGroup.notify(queue: .main) { [self] in // Wait for all queries to complete
var cycles: [SleepCycleData] = []
for (value, duration) in sleepPhaseDurations {
let heartRates = sleepCycleHeartRates[value] ?? []
let averageHeartRate: Double
if !heartRates.isEmpty {
averageHeartRate = heartRates.reduce(0.0, +) / Double(heartRates.count)
} else {
averageHeartRate = 0.0 // Assign a default value if no heart rates are available
}
let cycleData = SleepCycleData(phases: [value], duration: duration, heartRate: averageHeartRate)
cycles.append(cycleData)
// Store the cycleData in latestCycleData
self.latestCycleData = cycleData
}
DispatchQueue.main.async {
self.sleepCycles = cycles
self.latestCycleData = cycles.last // Update the latestCycleData property with the last cycle data
}
print("id = \(self.latestCycleData!.id),")
print("duration = \(self.formattedDuration(self.latestCycleData?.duration ?? 0.0)),")
print("phase = \(String(describing: self.latestCycleData?.phases)),")
print("hearth rate = \(String(describing: self.latestCycleData?.heartRate))")
}
}
healthStore.execute(query)
}
extension HealthStore {
func formattedDuration(_ duration: TimeInterval) -> String {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.allowedUnits = [.hour, .minute, .second]
return formatter.string(from: duration) ?? ""
}
}
i also have a DataModel:
import HealthKit
struct SleepCycleData: Identifiable, Codable {
let id = UUID()
let phases: [HKCategoryValueSleepAnalysis]
let duration: TimeInterval
let heartRate: Double // New property for heart rate
enum CodingKeys: String, CodingKey {
case phases, duration, heartRate
}
init(phases: [HKCategoryValueSleepAnalysis], duration: TimeInterval, heartRate: Double) {
self.phases = phases
self.duration = duration
self.heartRate = heartRate
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
phases = try container.decode([Int32].self, forKey: .phases).map { HKCategoryValueSleepAnalysis(rawValue: Int($0))! }
duration = try container.decode(TimeInterval.self, forKey: .duration)
heartRate = try container.decode(Double.self, forKey: .heartRate)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let phaseRawValues = phases.map { $0.rawValue }
try container.encode(phaseRawValues, forKey: .phases)
try container.encode(duration, forKey: .duration)
try container.encode(heartRate, forKey: .heartRate)
}
}
UPDATE
i'm also getting errors in the console in loop :
Failed to decode and load latest cycle data: valueNotFound(Swift.KeyedDecodingContainer<MacroChallenge.SleepCycleData.CodingKeys>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot get keyed decoding container -- found null value instead.", underlyingError: nil))