How can I get non-overlapping sleep samples from HealthKit in Swift?

469 Views Asked by At

I am trying to get sleep samples for last 7 days and store them and add whenever I want. I am using HKSampleQuery to fetch all the samples and storing samples which are only inBed samples but sometimes I am getting multiple samples with overlapped time intervals. How to get proper sleep data without any overlaps? I am using below code to get the data

func readSleep(from startDate: Date?, to endDate: Date?,Id:Int, Completion: @escaping (Double,Date,Date,Int,String)->Void) {
        var sleepingHoursCount = 0.0
        let healthStore = HKHealthStore()
        
        guard let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else {
            return
        }
        let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictEndDate)
        let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
        
        let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { (query, result, error) in
            if error != nil {
                return
            }
            guard let result = result else{
                Completion(sleepingHoursCount,startDate!,endDate!,Id,"")
                return
            }
//            do{
                if let sleepSamples = result as? [HKCategorySample] {
                    for sample in sleepSamples {
                        guard let sleepValue = HKCategoryValueSleepAnalysis(rawValue: sample.value) else {
                            return
                        }
                        let isInBed = sleepValue == .inBed
                        if(isInBed){
                            let diffSeconds = sample.endDate.timeIntervalSinceReferenceDate - sample.startDate.timeIntervalSinceReferenceDate
                            sleepingHoursCount += diffSeconds
                            Completion(diffSeconds/3600,sample.startDate,sample.endDate,Id,source)
                        }
                    }
                }
        }
        healthStore.execute(query)
    }
2

There are 2 best solutions below

0
Sowrya mukkavilli On BEST ANSWER

I tried this solution https://stackoverflow.com/a/63600255/13882733 which takes different samples with overlapping or non overlapping time intervals and skip the ones that overlap. it's working fine for me.

0
brendanjobrien On

This Swift function reads sleep data from the HealthKit store, which is a part of Apple's HealthKit framework that provides a central repository for health and fitness data on iPhone and Apple Watch.

The function readSleep(from:to:id:completion:) takes four parameters:

  • startDate: Date? and endDate: Date? are optional dates that indicate the start and end dates for which to fetch the sleep data.
  • id: Int seems to be some kind of identifier that is being passed back to the caller via the completion handler.
  • completion: @escaping (Double, Date, Date, Int, String) -> Void is a completion handler closure. This gets called when the sleep data has been fetched, and the closure receives the total sleep hours in seconds (Double), the start and end dates (Date), the passed identifier (Int), and a string parameter.

The function reads sleep data from the HealthKit store, calculates the total sleep duration (in seconds), and calls the completion handler with this data.

However, there are some areas in this function that can be improved:

  1. Error handling is not complete. When an error occurs while executing the HKSampleQuery, it simply returns from the function. It would be more appropriate to also call the completion handler and give the caller information about the error.

  2. The function uses force unwrapping for startDate and endDate in the completion closure. In Swift, force unwrapping is generally discouraged because it can lead to runtime errors if the value being unwrapped is nil. It would be better to safely unwrap these optional values.

  3. The function seems to be using a variable source that has not been defined within the scope of the function. Here is an improved version of the function that addresses these points:

func readSleep(from startDate: Date?, to endDate: Date?, id: Int, completion: @escaping (Double?, Date?, Date?, Int, String?) -> Void) {
    var sleepingHoursCount = 0.0
    let healthStore = HKHealthStore()
    
    guard let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else {
        completion(nil, nil, nil, id, "HealthKit Sleep Analysis data type not available.")
        return
    }
    
    let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictEndDate)
    let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
    
    let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { (query, result, error) in
        if let error = error {
            completion(nil, nil, nil, id, "Query Error: \(error.localizedDescription)")
            return
        }
        
        guard let result = result else {
            completion(nil, startDate, endDate, id, "No sleep data found.")
            return
        }
        
        if let sleepSamples = result as? [HKCategorySample] {
            for sample in sleepSamples {
                guard let sleepValue = HKCategoryValueSleepAnalysis(rawValue: sample.value) else {
                    completion(nil, nil, nil, id, "Invalid sleep data found.")
                    return
                }
                
                if sleepValue == .inBed {
                    let diffSeconds = sample.endDate.timeIntervalSinceReferenceDate - sample.startDate.timeIntervalSinceReferenceDate
                    sleepingHoursCount += diffSeconds
                    completion(diffSeconds / 3600, sample.startDate, sample.endDate, id, nil)
                }
            }
            completion(sleepingHoursCount / 3600, startDate, endDate,