Fetching and storing SleepData from HealthKit

79 Views Asked by At

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))

0

There are 0 best solutions below