search array of HKQuantitySample (steps taken) with predicate for NSDates

I have an array of HKSampleType for steps taken. I am trying to pickup one HKQuantitySample (steps taken) based on matching date. Turns out there is no date property. My code crashes saying there is no key called date, which is true.

How do I pickup one object from the array that is for the date I want.

Here is my code...


NSArray *stepsArray = caloriesHandle.stepsTakenSampleArray;
    
    NSPredicate *stepsPredicate = [NSPredicate predicateWithFormat:@"date=%@",historyDate];
    NSArray *stepsPredicateResultsArray = [stepsArray filteredArrayUsingPredicate:stepsPredicate];


when i change the format to @"endDate=%@",historyDate it doesn't return any results.


Could it be because of the time inside the historyDate? if yes, what is the solution.

Accepted Reply

If you are comparing against > previous day and < next day, it's perfectly correct to show the total of today and previous day. Because it includes the times. if your previous day is set to 0:00 and you check against > 0:00 then 0:01 already qualifies. Thats still data for yesterday, but thats what you asked for in your predicate.


Please see my previous answer again. You first need to start of the day for today. Then startDate needs to be bigger or equal to that and smaller to start of next day. So if you want everything for today, you need:

today at 00:00 <= startDate < tomorrow at 00:00

Replies

NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:historyDate];
    components.hour=components.minute=components.second=0;
    NSDate *currentDate =[[NSCalendar currentCalendar] dateFromComponents:components];

    NSArray *stepsArray = caloriesHandle.stepsTakenSampleArray;

    NSPredicate *stepsPredicate = [NSPredicate predicateWithFormat:@"startDate=%@",currentDate];
    NSArray *stepsPredicateResultsArray = [stepsArray filteredArrayUsingPredicate:stepsPredicate];


I changed the code as above. The filteredArrayUsingPredicate:stepsPredicate still doesn't give results. Using breakpoints I checked stepsPredicateResultsArray is still nil.




also, changed single equal to two equals for comparision


    NSPredicate *stepsPredicate = [NSPredicate predicateWithFormat:@"endDate==%@",currentDate];


I rewrote the code in a different way. Now, I'm making an array of dates in each HKQuantitySample, comparing the dates and then trying to retrive the HKQuantitySample for the given date. My code still crashed in the last line. Here's the code...


NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:historyDate];
    components.hour=components.minute=components.second=0;
    NSDate *currentDate =[[NSCalendar currentCalendar] dateFromComponents:components];
   
    NSArray *stepsArray = caloriesHandle.stepsTakenSampleArray;
   
    NSMutableArray *stepsDatesArray = [[NSMutableArray alloc] initWithCapacity:0];
    for (HKQuantitySample *stepsSample in stepsArray)
    {
        [stepsDatesArray addObject:stepsSample.startDate];
    }
    int i=0;
    int matchingIndex=0;
    for (NSDate *stepsDate in stepsDatesArray)
    {
        NSDateComponents *comps = [[NSCalendar currentCalendar] components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:stepsDate];
        components.hour=components.minute=components.second=0;
        NSDate *tempStepsDate =[[NSCalendar currentCalendar] dateFromComponents:comps];
        if ([tempStepsDate compare:currentDate])
        {
            matchingIndex = i;
            break;
        }
        else
        {
            i++;
            continue;
        }
    }
   
    HKQuantitySample *stepsSample = stepsArray[matchingIndex];

I get outofbounds exception in the last line of code. Index being 0.

Here is the error message...


Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 18446744073709551615 beyond bounds for empty NSArray'
*** First throw call stack:
(
  0   CoreFoundation                      0x00007fff23e3dcce __exceptionPreprocess + 350
  1   libobjc.A.dylib                     0x00007fff50b3b9b2 objc_exception_throw + 48
  2   CoreFoundation                      0x00007fff23d05a7e -[__NSArray0 objectAtIndex:] + 94
  3   Pace                                0x000000010e26ec43 -[HistoryOperator createHistoryObjectForDate:] + 2435
  4   Pace                                0x000000010e26d717 -[HistoryOperator loadHistoryObjectsForSettingsArray:] + 903
  5   Pace                                0x000000010e26ab7c -[HistoryVC loadHistory] + 124
  6   Pace                                0x000000010e26b488 -[HistoryVC unwindToHistoryVC:] + 152
  7   UIKitCore                           0x00007fff48d933fb -[UIStoryboardUnwindSegueTemplate _performWithDestinationViewController:sender:] + 214
  8   UIKitCore                           0x00007fff48d932f2 -[UIStoryboardUnwindSegueTemplate _perform:] + 83
  9   UIKitCore                           0x00007fff48d915c4 -[UIStoryboardSegueTemplate perform:] + 157
  10  UIKitCore                           0x00007fff48c1a4d5 -[UIApplication sendAction:to:from:forEvent:] + 83
  11  UIKitCore                           0x00007fff485cbc83 -[UIControl sendAction:to:forEvent:] + 223
  12  UIKitCore                           0x00007fff485cbfcb -[UIControl _sendActionsForEvents:withEvent:] + 396
  13  UIKitCore                           0x00007fff485caf3c -[UIControl touchesEnded:withEvent:] + 497
  14  UIKitCore                           0x00007fff48c55d10 -[UIWindow _sendTouchesForEvent:] + 1359
  15  UIKitCore                           0x00007fff48c57a95 -[UIWindow sendEvent:] + 4501
  16  UIKitCore                           0x00007fff48c31ed9 -[UIApplication sendEvent:] + 356
  17  UIKitCore                           0x00007fff48cbc336 __dispatchPreprocessedEventFromEventQueue + 7328
  18  UIKitCore                           0x00007fff48cbf502 __handleEventQueueInternal + 6565
  19  UIKitCore                           0x00007fff48cb606b __handleHIDEventFetcherDrain + 88
  20  CoreFoundation                      0x00007fff23da1c71 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
  21  CoreFoundation                      0x00007fff23da1b9c __CFRunLoopDoSource0 + 76
  22  CoreFoundation                      0x00007fff23da1374 __CFRunLoopDoSources0 + 180
  23  CoreFoundation                      0x00007fff23d9bf6e __CFRunLoopRun + 974
  24  CoreFoundation                      0x00007fff23d9b884 CFRunLoopRunSpecific + 404
  25  GraphicsServices                    0x00007fff38b5ac1a GSEventRunModal + 139
  26  UIKitCore                           0x00007fff48c19220 UIApplicationMain + 1605
  27  Pace                                0x000000010e26980a main + 122
  28  libdyld.dylib                       0x00007fff519b910d start + 1
  29  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

You are checking against a certain date with time. There most likely is no set of data for that moment. You should use a date range. So something like "startDate > %@ AND startDate < %@".


Also check out this function in HKQuery. That's basically what you want

open class func predicateForSamples(withStart startDate: Date?, end endDate: Date?, options: HKQueryOptions = []) -> NSPredicate

Not a date range but an exact match.

Well a certain date is also a date range. Let’s say you want everything on the 18th of April 2020, then you check against >= 18th of April at 00:00 and < 19th of April at 00:00. You can also use smaller ranges, like hours or minutes. I don’t think it makes any sense to go even smaller than minutes. Your taken steps are saved with date and time. If you just check for "date == 18th of April” there will still be some time where you probably won’t have any data for.

Got it. That sounds far simpler.

I changed the code as follows


    NSDateComponents *dayComponent = [[NSDateComponents alloc] init];
    dayComponent.day = -1;
    NSCalendar *currentCalendar = [NSCalendar currentCalendar];
    NSDate *previousDate = [currentCalendar dateByAddingComponents:dayComponent toDate:historyDate options:0];
    dayComponent.day = 1;
    NSDate *nextDate = [currentCalendar dateByAddingComponents:dayComponent toDate:historyDate options:0];
   
    NSPredicate *stepsPredicate = [NSPredicate predicateWithFormat:@"endDate>%@ && endDate<%@",previousDate,nextDate];
    NSArray *stepsPredicateResultsArray = [stepsArray filteredArrayUsingPredicate:stepsPredicate];

My swift version looks like this:


let today = Calendar.current.startOfDay(for: Date())
let tomorrow = Calendar.current.nextDate(after: today, matching: DateComponents(hour: 0), matchingPolicy: .nextTimePreservingSmallerComponents) ?? Date()
let predicate = NSPredicate(format: "startDate >= %@ AND startDate < %@", today as NSDate, tomorrow as NSDate)

dont understand why it is returning results for previous date as well? i think it might have something to do with iOS storing dates in GMT.


have used > instead of >=


i'll try it with startDate


Here is the code...


NSArray *stepsArray = caloriesHandle.stepsTakenSampleArray;
   
    //make previous and next date
    NSDateComponents *dayComponent = [[NSDateComponents alloc] init];
    dayComponent.day = -1;
    NSCalendar *currentCalendar = [NSCalendar currentCalendar];
    NSDate *previousDate = [currentCalendar dateByAddingComponents:dayComponent toDate:historyDate options:0];

    dayComponent.day = 1;
    NSDate *nextDate = [currentCalendar dateByAddingComponents:dayComponent toDate:historyDate options:0];
   
    //filter aray for current date and calculate steps for that date and set steps
    NSPredicate *stepsPredicate = [NSPredicate predicateWithFormat:@"startDate>%@ AND startDate<%@",previousDate,nextDate];
    NSArray *stepsPredicateResultsArray = [stepsArray filteredArrayUsingPredicate:stepsPredicate];
    int64_t steps=0;
    for (HKQuantitySample *stepsSample in stepsPredicateResultsArray)
    {
        HKQuantity *stepsQuantity = [stepsSample quantity];
        steps = steps + (int64_t)[stepsQuantity doubleValueForUnit:[HKUnit countUnit]];
    }
    historyObject.stepsTaken=steps;

it gives me the total of today and previous day.

iOS is storing date in UTC and not GMT.


please help me with this one.

If you are comparing against > previous day and < next day, it's perfectly correct to show the total of today and previous day. Because it includes the times. if your previous day is set to 0:00 and you check against > 0:00 then 0:01 already qualifies. Thats still data for yesterday, but thats what you asked for in your predicate.


Please see my previous answer again. You first need to start of the day for today. Then startDate needs to be bigger or equal to that and smaller to start of next day. So if you want everything for today, you need:

today at 00:00 <= startDate < tomorrow at 00:00

NSPredicate *stepsPredicate = [NSPredicate predicateWithFormat:@"startDate>=%@ AND startDate<%@",today,nextDate]; Changing previousDate to today did it.