Restrict HKSampleQuery results to those input from your own app - ios

I wish to restrict results returned from a HealthKit HKSampleQuery to those that have been input through my own app. Is there a way to specify results only with my application bundle identifier, and thereby exclude any other data sources returned from other applications?
Is there a way to specify this with an NSSortDescriptor or NSPredicate, as I have tried below?
func querySteps() {
// let sort = NSSortDescriptor(key: "bundleIdentifier", ascending: true, selector: "com.companyName.appName:")
// let resultPredicate = NSPredicate(format: "bundleIdentifier", "com.companyName.appName")
let sampleQuery = HKSampleQuery(sampleType: healthKitManager.stepsCount!,
predicate: nil,
limit: 100,
sortDescriptors: nil)
{ [unowned self] (query, results, error) in
if let results = results as? [HKQuantitySample] {
self.steps = results
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
});
}
}
healthStore?.executeQuery(sampleQuery)
}

Simple one line solution to the question above. Use HKQuery to create a predicate object that specifies the data's source:
let thePredicate = HKQuery.predicateForObjectsFromSource(HKSource.defaultSource())
And then swap out the nil predicate parameter value with thePredicate, in this case. Then, the results in your table view will show only your own app's HKQuery results.

Related

How to get heart rate data from HealthKit for each workout

I am attempting to retrieve heart rate for all workouts completed and also start and stop times for the workout and return all values as Times or Quantity items in my JSON result. I have been able to get all workouts by querying for duration greater than 0, but now I would like to get this additional info.
let workoutPredicate = HKQuery.predicateForWorkouts(with: .greaterThanOrEqualTo, duration: 1)
let compound = NSCompoundPredicate(andPredicateWithSubpredicates:
[workoutPredicate])
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate,
ascending: true)
let query = HKSampleQuery(
sampleType: .workoutType(),
predicate: compound,
limit: 0,
sortDescriptors: [sortDescriptor]) { query, samples, error in
DispatchQueue.main.async {
guard let finalSamples = samples as? [HKWorkout] else {
result()
return
}
result(finalSamples.map { sample -> NSDictionary in
return ["duration" : sample.duration, "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.mile()) as Any, "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()) as Any]
})
}
}
HKHealthStore().execute(query)
I would like to get the heart rate data for the workout and workout start/end time.
A little late but start & end date/time per workout should be easy with:
print("started #: \(sample.startDate), ended #: \(sample.endDate)")
Did you manage to get heart rates (min/max/avg) per workout? I am also interested to get this & can't seem to figure out if this is possible or not?

Efficient way of sectioning data based on date for usage in Collection View

I am trying to section user's Photo Library pictures in a Collection View based on their creation date in ascending order. I am using this method which obviously is painfully slow especially when the number of pictures is high.
First I fetch the PHAssets in sorted order:
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
inFetchResult = PHAsset.fetchAssets(with: allPhotosOptions)
and then I copy the assets inside an array:
inFetchResult.enumerateObjects { (asset, index, stop) in
let yearComponent = Calendar.current.component(.year, from: asset.creationDate!)
let monthComponent = Calendar.current.component(.month, from: asset.creationDate!)
let monthName = DateFormatter().monthSymbols[monthComponent - 1]
var itemFound = false
for (index, _) in self.dateArray.enumerated() {
if self.dateArray[index].date == "\(monthName) \(yearComponent)" {
self.dateArray[index].assets.append(asset)
itemFound = true
break
} else {
continue
}
}
if !itemFound {
self.dateArray.append((date: "\(monthName) \(yearComponent)", assets: [asset]))
}
}
I then use this array as my data source.
Is there a better way to do this? I have tried dictionaries but they change the order of the objects. I also have considered finding a way to only add the assets to my dateArray when they are going to be displayed on view, however, collection view needs to know the total number of sections up front so I must go through all of the pictures and check their dates before loading the view.
You can fetch photo assets and do all the sorting logic in background
thread like in the below code, once you will done with the heavy
processing then you can access main Thread to update the UI.
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
DispatchQueue.global(qos: .userInitiated).async {
let photos = PHAsset.fetchAssets(with: .image, options: nil)
var result = [Any]()
photos.enumerateObjects({ asset, _, _ in
// do the fetched photos logic in background thread
})
DispatchQueue.main.async {
// once you will get all the data, you can do UI related stuff here like
// reloading data, assigning data to UI elements etc.
}
}

HKAnchoredObjectQuery updateHandler not being called

I have a UITableView backed by an HKAnchoredObjectQuery. My resultsHandler is called fine, but my updateHandler is never called. I am not looking for background delivery, just live foreground delivery when a new sample is added to the health store. According to the docs on the updateHandler:
If this property is set to nil, the anchor query automatically stops
as soon as it has finished calculating the initial results. If this
property is not nil, the query behaves similarly to the observer
query. It continues to run, monitoring the HealthKit store. If any
new, matching samples are saved to the store—or if any of the existing
matching samples are deleted from the store—the query executes this
update handler on a background queue.
I do set my updateHandler on the query, and there are being new samples written to the health store.
In my viewDidLoad I set up my query:
override func viewDidLoad() {
super.viewDidLoad()
// Setup TableView
tableView.dataSource = self
tableView.delegate = self
let calendar = Calendar.current
let nextDay = calendar.date(byAdding: .day, value: 1, to: self.date)!
let startOfNextDay = calendar.startOfDay(for: nextDay)
let resultsHandler: HKAnchoredObjectQueryHandler = { [weak self](query, samples, deletedObjects, anchor, error) in
guard let strongSelf = self else { return }
guard let samples = samples as? [HKCorrelation],
let _ = deletedObjects else {
// Need error handling here
return debugPrint("Query error occurred: \(error!.localizedDescription)")
}
DispatchQueue.main.async {
// Get a reference to the query and anchor
strongSelf.query = query
strongSelf.anchor = anchor
// Add new samples
strongSelf.foodEntries = samples
for entry in strongSelf.foodEntries {
debugPrint(entry.metadata?[HKMetadataKey.mealOfDay] as! Int)
}
}
}
let updateHandler: HKAnchoredObjectQueryHandler = {[weak self](query, samples, deletedObjects, anchor, error) in
guard let strongSelf = self else { return }
guard let samples = samples as? [HKCorrelation],
let deletedObjects = deletedObjects else {
// Need error handling here
return debugPrint("Query error occurred: \(error!.localizedDescription)")
}
DispatchQueue.main.async {
// Get a reference to
strongSelf.anchor = anchor
print(#line, "HKAnchoredObjectQuery IS LONG RUNNING")
// Add new samples
strongSelf.foodEntries.append(contentsOf: samples)
// Remove deleted samples
let deletedObjects = deletedObjects
for object in deletedObjects {
if let index = strongSelf.foodEntries.index(where: {$0.uuid == object.uuid} ) {
strongSelf.foodEntries.remove(at: index)
}
}
strongSelf.tableView.reloadData()
}
}
HealthManager.shared.anchoredFoodQuery(startDate: self.date, endDate: startOfNextDay, anchor: anchor, resultsHandler: resultsHandler, updateHandler: updateHandler)
}
The anchored object methods in my HealthManager are:
func anchoredFoodQuery(startDate: Date, endDate: Date, anchor: HKQueryAnchor? = nil, resultsHandler: #escaping HKAnchoredObjectQueryHandler, updateHandler: HKAnchoredObjectQueryHandler?) {
let predicate = HKSampleQuery.predicateForSamples(withStart: startDate, end: endDate, options: [.strictStartDate, .strictEndDate]) //NSPredicate(format: "date => %# && date < %#", startDate as NSDate, endDate as NSDate)
guard let foodIdentifier = HKCorrelationType.correlationType(forIdentifier: .food) else {
fatalError("Can't make food Identifier")
}
anchoredQuery(for: foodIdentifier, predicate: predicate, resultsHandler: resultsHandler, updateHandler: updateHandler)
}
/// Wrapper to create and execute a query
private func anchoredQuery(for sampleType: HKSampleType, predicate: NSPredicate?, anchor: HKQueryAnchor? = nil, limit: Int = HKObjectQueryNoLimit, resultsHandler: #escaping HKAnchoredObjectQueryHandler , updateHandler: HKAnchoredObjectQueryHandler?) {
let query = HKAnchoredObjectQuery(type: sampleType,
predicate: predicate,
anchor: anchor,
limit: limit,
resultsHandler: resultsHandler)
query.updateHandler = updateHandler
healthStore.execute(query)
}
My UITableViewController was in a UIPageViewController. viewDidAppear() on the UITableViewController does not get called when returning from the detail screen, so I assumed viewDidDisappear() would not get called as well and I was stopping the query in there. My assumption was wrong.
A question on where to stop long running HKQuerys: Do HKQuery's need to be stopped in View Controllers

How to synchronize return values from asynchronous HealthKit queries?

I've written a function that retrieves max heart rate and average heart rate from HKHealthStore. Though both HKStatisticQuery() calls work, due to the fact that completion handlers are asynchronous, the return values (maxHRT, avgHRT) for the function are (0, 0).
What is an elegant way to wait for both completion handlers to return, before updating return values and exiting function?
Code:
func calcHeartRateStatistics() -> (Double, Double){
var maxHRT:Double = 0
var avgHRT:Double = 0
// First specify type of sample we need, i.e. Heart Rate Type
//
let sampleType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
// Create query predicate so result only returns objects within the period between Start Date
// and End Date
//
let predicate = HKQuery.predicateForSamplesWithStartDate(pvtStartDate, endDate: pvtEndDate, options: .None)
// Query for Maximum Heart Rate that occurred between Start Date and End Date
//
let queryMaxHeartRate = HKStatisticsQuery(quantityType: sampleType!,
quantitySamplePredicate: predicate,
options: .DiscreteMax)
{ [unowned self] (query, result, error) in
if let maxQuantity = result?.maximumQuantity() {
let maxHeartRate = maxQuantity.doubleValueForUnit(HKUnit(fromString: "count/min"))
maxHRT = round(maxHeartRate)
}
}
// Query for Average Heart Rate that occurred between Start Date and End Date
//
let queryAverageHeartRate = HKStatisticsQuery(quantityType: sampleType!,
quantitySamplePredicate: predicate,
options: .DiscreteAverage)
{ [unowned self] (query, result, error) in
if let averageQuantity = result?.averageQuantity(){
let avgHeartRate = averageQuantity.doubleValueForUnit(HKUnit(fromString: "count/min"))
avgHRT = round(avgHeartRate)
}
}
pvtHealthStore.executeQuery(queryMaxHeartRate)
pvtHealthStore.executeQuery(queryAverageHeartRate)
return (maxHRT, avgHRT)
} // calcHeartRateStatistics

startDate, endDate, and adding text to label in ViewController

So I am having a few bugs and errors in my ViewController code in my app. First, in my line:
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate,endDate: endDate ,options: .None)
I get the following error: use of unresolved identifier 'endDate'
This is weird. Is it because I have not made them into NSDate objects? How would I go about doing that?
As for my second question, it is not exactly a bug, I simply do not know how to do something. I have connected my label from the storyboard into my view controller. I want to get the data I collect to simply print onto the screen in the label. I know how to do this in most situations but I am lost as to how I can do this within my current configuration. I don't want the user to press a button or anything, I just want it to automatically display by default...Any help is greatly appreciated!!!
I have included all my code below:
import UIKit
import HealthKit
class ViewController: UIViewController {
#IBOutlet weak var displayData: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// create store
let healthStore = HKHealthStore()
// create an object type to request an authorization for a specific category, here is SleepAnalysis
if let sleepType = HKObjectType.categoryTypeForIdentifier(HKCategoryTypeIdentifierSleepAnalysis) {
let setType = Set<HKSampleType>(arrayLiteral: sleepType)
healthStore.requestAuthorizationToShareTypes(setType, readTypes: setType, completion: { (success, error) -> Void in
// here is your code
})
}
if let sleepType = HKObjectType.categoryTypeForIdentifier(HKCategoryTypeIdentifierSleepAnalysis) {
// we create a predicate to filter our data
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate,endDate: endDate ,options: .None)
// I had a sortDescriptor to get the recent data first
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
// we create our query with a block completion to execute
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: 30, sortDescriptors: [sortDescriptor]) { (query, tmpResult, error) -> Void in
endDate is undefined - you have not actually created a variable that represents endDate - thus the compiler is telling you that. And, also, startDate is also undefined, even though the compiler hasn't told you that yet. In the least, you need to create NSDates.
let startDate = NSDate()
let endDate = NSDate()
More specifically, you'll need to create them in the range for which you want to query.
Regarding showing the data by default, simply set the UILabel's text in viewDidLoad. I'm assuming that you want to set this text in response to the HealthKit query? If so, you can still do that in viewDidLoad
So, to use your query, it needs to be provided to executeQuery on healthStore Here's some code that you could use with what you posted:
healthStore.executeQuery(HKSampleQuery(
sampleType: sleepType,
predicate: predicate,
limit: 30,
sortDescriptors: [sortDescriptor],
resultsHandler: { (query: HKSampleQuery!, results: [AnyObject]!, err: NSError?) -> Void in
if err != nil {
// ERROR Occurred, handled it
println(err)
return
}
var labelText = ""
for result in results as [HKQuantitySample]! {
// SUCCESS, use results here
labelText += result
}
displayData.text = labelText
}
))

Resources