I need to plot graph for steps taken by user on hourly basis on any specific date. But if the user's steps start today at 3:58 pm and end today at 4:10 pm then I am getting just one HKStatistics object for this period of time. I am not able to break this data into two samples as I need to get steps taken in the 3-4 pm slot and the 4-5 pm slot.
static func getSteps(date: Date, duration: DateComponents, completion: #escaping ([HKSample]) -> Void) {
let quantityType : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!]
let stepsQuantityType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let startOfDay = Calendar.current.startOfDay(for: date)
if let endOfDay = Calendar.current.date(byAdding: duration, to: startOfDay) {
var interval = DateComponents()
interval.hour = 1
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: endOfDay, options: .strictStartDate)
let query = HKSampleQuery.init(sampleType:stepsQuantityType,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil,
resultsHandler: { (query, results, error) in
guard let result = results else {
return
}
// print("result healthkit",result.description)
//print("Total count:",)
completion(result)
})
healthStore.execute(query)
}
}
Don't use HKSampleQuery for charting quantity types. HKStatisticsCollectionQuery is designed for this purpose and will split samples that fall into separate regions of your chart for you. See the documentation for examples of how to build the query and use its results.
You're correct, you can't split the sample. That's the all the information that's available. Steps are not stored step-by-step; they're aggregated into blocks to reduce power and storage requirements (mostly power; it's easier to accumulate a value in hardware and periodically read it than to query the real time clock every single time a step is detected).
In order to do what you're discussing, you'll need to average the steps over the period. So if there were 100 steps over the period 3:58p to 4:07p, that averages 10 steps/minute, and you would allocate 20 steps to the 3p-4p block and 80 steps to the 4p-5p block. That's the best information you have.
Related
I have a iOS Widget that I am trying to update every 5 or 15 minutes.
I am new to widgets and do not understand how to loop the timeline with an async call.
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let price: Double
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
networkManager.fetchData { price in
var entries: [SimpleEntry] = []
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration, price: price)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
I have a completion handler that passes in the price from the async api call.
networkManager.fetchData { price in
}
It looks like your networkManager.fetchData is providing only one price value at a time.
In such cases, when only one entry is provided at a time then the following should be enough:
func getTimeline(for configuration: ConfigurationIntent,
in context: Context,
completion: #escaping (Timeline<Entry>) -> ()) {
networkManager.fetchData { (price) in
let currentDate = Date()
//create the entry for the given price
let entry = SimpleEntry(date: currentDate,
configuration: configuration,
price: price)
/*
If you can predict price values then you need the loop for multiple entries.
However in your case, it seems this one entry is sufficient
*/
let entries = [entry]
//next reload date; 15mins in this case
let reloadDate = Calendar.current.date(byAdding: .minute,
value: 15,
to: currentDate)!
//your timeline with one entry that will refresh after given date
let timeline = Timeline(entries: entries, policy: .after(reloadDate))
completion(timeline)
}
}
By specifying after(_ date:) as the TimelineReloadPolicy, iOS will refresh the widget after the given date.
The timeline’s refresh policy specifies the earliest date for WidgetKit to request a new timeline from the provider. The default refresh policy, .atEnd, tells WidgetKit to request a new timeline after the last date in the array of timeline entries you provide. However, you can use .afterDate to indicate a different date either earlier or later than the default date. Specify an earlier date if you know there’s a point in time before the end of your timeline entries that may alter the timeline.
Ref: Apple Documentation on Timeline
This solution was to address the core issue, so finally I would like to add a disclaimer that reloading every 15mins might be overkill. Hence tweak your logic to specify the TimelineReloadPolicy intelligently. If you can predict values & create a timeline of multiple entries with fewer network calls then great! If not then... well... best of luck :)
Important
Plan ahead if your widget makes requests to a server when it reloads, and uses afterDate() with a specific date in timeline entries. WidgetKit tries to respect the date you specify, which may cause a significant increase in server load when multiple devices reload your widget at around the same time.
Ref: Keeping a Widget Up To Date
More Read:
Apple Documentation on TimelineProvider
I want to retrieve steps from 1h ago. I don't need to do anything special, I just need to know how many steps a user has done since the last hour.
Even though my iPhone has some steps logged, the query to retrieve the number of steps returns "nil".
This is the code:
let calendar = Calendar.current //calendar now, to be used in calculating the h in the past
let beforeDate = calendar.date(byAdding: .hour, value: -1, to: Date())
let pedometer = CMPedometer() //define pedometer
if CMPedometer.isStepCountingAvailable() == true {print("steps available")}else{print("steps not available")}
pedometer.queryPedometerData(from: beforeDate!, to: Date(), withHandler: { (pedometerData, error) in
if let pedData = pedometerData{
self.dateLabel.text = "Steps:\(pedData.numberOfSteps)"
}else {
self.dateLabel.text = "error)"
print(beforeDate)
}
})
}
And this is the date format that I put in the query:
2018-03-16 12:59:17 +0000
What is wrong?
One possible problem is that your CMPedometer object is stored only in a local variable. pedometer.queryPedometerData runs asynchronously, so this object needs to persist long enough to fulfill the query. But it can't do that if it is a local variable; it vanishes before the data can even be fetched. Try making pedometer a persistent instance property instead.
Also be aware that you don't know what queue the data will be delivered on. You need to step out to the main queue in order to talk to the interface, and you are failing to do that.
I'm trying to set up an HKAnchoredObjectQuery that will only deliver results from the last time I made this query, but I can't get my head around the logic in setting up my HKQueryAnchor and how I persist it? In Apple's sample code they do not show the initial declaration for the HKQueryAnchor. Do I need to store locally the date of the last sample I downloaded and construct an anchor from that date? This code below returns every sample in HealthKit.
func updateWorkouts(completionHandler: #escaping () -> Void) {
var anchor: HKQueryAnchor?
let sampleType = HKObjectType.workoutType()
let workoutPredicate = HKQuery.predicateForWorkouts(with: .hockey)
let sourcePredicate = HKQuery.predicateForObjects(from: HKSource.default()) //limit query to only this app
let compound = NSCompoundPredicate(andPredicateWithSubpredicates: [workoutPredicate, sourcePredicate])
let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: compound, anchor: anchor, limit: HKObjectQueryNoLimit) { [unowned self] query, newSamples, deletedSamples, newAnchor, error in
self.handleNewWorkouts(newWorkoutsAsSamples: newSamples!, deleted: deletedSamples!)
anchor = newAnchor
completionHandler()
}
healthStore.execute(anchoredQuery)
}
When initializing an HKAnchoredObjectQuery, you are expected to either provide nil or an anchor object that you received from a query that you executed previously. You cannot directly construct an HKQueryAnchor yourself. To persist an anchor between application launches, you can encode it in persistent storage using NSKeyedArchiver. It is common to store the resulting encoded NSData in NSUserDefaults.
I have an iOS app that I'm developing in Swift, and as part of the app it gathers the step count for the current day.
The first time I run the app, the count is "0", but if I click a button in the interface to re-run this function that queries HK, then the correct number appears.
I am guessing this is because HK needs some time to gather the data, or something, but I'm not sure how to fix it. Maybe HK can fire an event when the data is ready, and then I can update the UI?
Here's the routine that gathers the data from HK. This function executes immediately when the app start (and it shows "0 steps today"), and then as I describe above, I can tap a button to execute the function again (and then I get the right number).
func queryStepsSum() {
// prepare HK for the data
let endDate = NSDate() // right now
let startDate = NSCalendar.currentCalendar().dateBySettingHour(0, minute: 0, second: 0, ofDate: endDate, options: NSCalendarOptions())
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: .None)
let sumOption = HKStatisticsOptions.CumulativeSum
let statisticsSumQuery = HKStatisticsQuery( quantityType: self.stepsCount!, quantitySamplePredicate: predicate,
options: sumOption)
{ [unowned self] (query, result, error) in
if let sumQuantity = result?.sumQuantity() {
self.numberOfSteps = Int(sumQuantity.doubleValueForUnit(HKUnit.countUnit()))
}
}
// run the HK query
self.healthStore?.executeQuery(statisticsSumQuery)
// update the UI with the result
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.stepsLabel.text = "\(self.numberOfSteps) steps today";
});
}
Your code doesn't wait for the query to finish before displaying the result (the query happens asynchronously). You should update your UI from the query's result handler, like this:
let statisticsSumQuery = HKStatisticsQuery( quantityType: self.stepsCount!, quantitySamplePredicate: predicate,
options: sumOption)
{ [unowned self] (query, result, error) in
dispatch_async(dispatch_get_main_queue()) {
if let sumQuantity = result?.sumQuantity() {
self.numberOfSteps = Int(sumQuantity.doubleValueForUnit(HKUnit.countUnit()))
self.stepsLabel.text = "\(self.numberOfSteps) steps today";
}
}
}
Also note that I've included a dispatch back to the main thread since the query results handler runs on a background thread and it's not safe to manipulate UI in that context.
Are you calling your function in viewDidLoad? You might try calling it in viewDidAppear instead.
If that doesn't work maybe there is some sort of delegate method that can update you when the data is ready like you said. I've never worked with HealthKit but I'll check to see if I can find anything.
EDIT:
This might set the UI component off the background thread.
Try:
var numberOfSteps: Int! {
didSet {
self.stepsLabel.text = "\(self.numberOfSteps) steps today"
}
}
That should get rid of the error that you're updating the UI on the background thread. Also make sure to remove your call in the closure that I told you to put.
I am trying to understand how HKAnchoredObjectQuery works. Once the workout started and workout session state changes to running, I call the following function to execute the query and get the Heart Beat Value.
func createHeartRateStreamingQuery() {
guard let quantityType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate) else { return nil }
var heartRateQuery : HKAnchoredObjectQuery? = HKAnchoredObjectQuery(type: quantityType, predicate: nil, anchor: nil, limit: Int(HKObjectQueryNoLimit)) { (query, sampleObjects, deletedObjects, newAnchor, error) -> Void in
}
heartRateQuery!.updateHandler = {(query, samples, deleteObjects, newAnchor, error) -> Void in
{
//Samples only have 1 entry which is the most recent reading.
}
}
self.healthStore.executeQuery(heartRateQuery!)
}
HeartRateQuery's update handler is called every 2 to 3 seconds and samples variable in the completion handler is having only 1 reading of the Heart Rate which is the most current reading. Shouldn't it have all the readings of Heart Rate since the workout started since I have not set any limits, predicate or anchor on the query?
The behavior you are seeing is expected. The updateHandler is only called with samples that are new since the handler was last invoked. If you want to keep track of the samples recorded during the workout then you should add them to an array each time the handler is called.
Note that because you are not using a predicate, the initial results block will include all heart rate samples that are currently available in HealthKit, not just the samples recorded during the workout session. You should probably constrain the query with a date predicate to only get the samples you are interested in.