Building a HealthKit/WatchKit app based off WWDC 2015 - Session 203.
There is no source code so I am writing it on the fly. There is a method I am having difficulty with since they don't discuss it.
Luckily it's the same addQuantitiesFromSamples method for all the workout types that add sample quantities to the workout session.
Of course I have this error because that method does not exist in my code.
Value of type 'HKQuantity' has no member 'addQuantitiesFromSamples'
I am not sure how to write a method that adds sample quantities. The method must be relatively basic because it's being used on all three of the sample queries in the project.
The sumDistanceSamples function is where the mystery addQuantitiesFromSamples method is called.
This is one of the three blocks containing the same error so I just need to find a solution for one of them.
WorkoutSessionManager.swift
class WorkoutSessionManager: NSObject, HKWorkoutSessionDelegate {
var activeEnergySamples: [HKQuantitySample] = []
var distanceSamples: [HKQuantitySample] = []
var heartRateSamples: [HKQuantitySample] = []
// ... code
var distanceType: HKQuantityType {
if self.workoutSession.activityType == .Cycling {
return HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceCycling)!
} else {
return HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)!
}
}
var currentActiveEnergyQuantity: HKQuantity
var currentDistanceQuantity: HKQuantity
var currentHeartRateSample: HKQuantitySample?
// ... code
// MARK: Data queries
// Create streaming query helper method.
func createStreamingDistanceQuery(workoutStartDate: NSDate) -> HKQuery? {
guard let quantityType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning) else {return nil}
// Instantiate a HKAnchoredObjectQuery object with a results handler.
let distanceQuery = HKAnchoredObjectQuery(type: quantityType, predicate: nil, anchor: anchor, limit: Int(HKObjectQueryNoLimit)) { (query, samples, deletedObjects, newAnchor, error) -> Void in
guard let newAnchor = newAnchor else {return}
self.anchor = newAnchor
self.sumDistanceSamples(samples)
}
// Results handler that calls sumDistanceSamples function.
distanceQuery.updateHandler = {(query, samples, deletedObjects, newAnchor, error) -> Void in
self.anchor = newAnchor!
self.sumDistanceSamples(samples)
}
return distanceQuery
}
func sumDistanceSamples(samples: [HKSample]?) {
guard let currentDistanceSamples = samples as? [HKQuantitySample] else { return }
dispatch_async(dispatch_get_main_queue()) {
// Error point - "no member 'addQuantitiesFromSamples'"
self.currentDistanceQuantity = self.currentDistanceQuantity.addQuantitiesFromSamples(currentDistanceSamples, unit: self.distanceUnit)
// Add sample to array of samples accumulated over the workout.
self.distanceSamples += currentDistanceSamples
self.delegate?.workoutSessionManager(self, didUpdateDistanceQuantity: self.currentDistanceQuantity)
}
}
// MARK: HEART RATE STREAMING
func createHearRateStreamingQuery(workoutStartDate: NSDate) -> HKQuery? {
// alternative method to creating a match samples predicate
// Append the new quantities with the current heart rate quantity.
guard let quantityType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate) else {return nil}
// Instantiate a HKAnchoredObjectQuery object with a results handler that calls our sumHeartRateSamples function
let heartRateQuery = HKAnchoredObjectQuery(type: quantityType, predicate: nil, anchor: anchor, limit: Int(HKObjectQueryNoLimit)) { (query, samples, deletedObjectts, newAnchor, error) -> Void in
guard let newAnchor = newAnchor else {return}
self.anchor = newAnchor
self.updateHeartRateSamples(samples)
}
// Results handler that calls our addActiveEnergySamples function
heartRateQuery.updateHandler = {(query, samples, deletedObjects, newAnchor, error) -> Void in
self.anchor = newAnchor!
self.updateHeartRateSamples(samples)
}
return heartRateQuery
}
func updateHeartRateSamples(samples: [HKSample]?) {
guard let heartRateCountSamples = samples as? [HKQuantitySample] else { return }
// updateHeartRateSamples method dispatches back to the main queue.
dispatch_async(dispatch_get_main_queue()) {
// Error: Value of type 'HKQuantitySample?' has no member 'addQuantitiesFromSamples
self.currentHeartRateSample = self.currentHeartRateSample.addQuantitiesFromSamples(heartRateCountSamples, unit: self.countPerMinuteUnit)
// appends/updates that sample to an array of samples accumulated over the workout.
self.heartRateSamples += heartRateCountSamples
self.delegate?.workoutSessionManager(self, didUpdateHeartRateSample: self.currentHeartRateSample!)
}
}
I'm not sure if this is what you're looking for, but this appears to be your missing method from someone else who watched the same WWDC video:
extension HKQuantity {
func addQuantitiesFromSamples(samples : [HKQuantitySample], unit: HKUnit) -> HKQuantity {
var currentValue = doubleValueForUnit(unit)
for sample in samples {
let value = sample.quantity.doubleValueForUnit(unit)
currentValue += value
}
let result = HKQuantity(unit: unit, doubleValue: currentValue)
return result
}
}
Source: Calories and Distance data from query
Related
I'm working on a watchOS App as my first Swift/iOS project ever. I want to fetch the latest body weight sample and use it for some calculation. The result is presented to the user. As soon as a new sample is added, I want to update my UI as well. It works in a completely fresh simulator installation. As soon as I add a sample in the iOS simulator, the app updates its UI in the watchOS simulator. However, it doesn't work on my real device or after resetting the watchOS simulator. And I just don't know why. The HKAnchoredObjectQuery just returns 0 samples but I definitely have some samples stored in health. I can even see them under Settings > Health on my watch. I can't imagine this is related to my code, but here it is:
class WeightProvider: ObservableObject {
private static let weightSampleType = HKSampleType.quantityType(forIdentifier: .bodyMass)!
private static let healthStore: HKHealthStore = .init()
private var previousAnchor: HKQueryAnchor?
private var runningQuery: HKAnchoredObjectQuery?
#Published var bodyWeight: Measurement<UnitMass>?
func getBodyWeight(longRunning: Bool = false) {
let query = HKAnchoredObjectQuery(type: Self.weightSampleType, predicate: nil, anchor: previousAnchor, limit: longRunning ? HKObjectQueryNoLimit : 1, resultsHandler: processQueryResult)
if longRunning {
query.updateHandler = processQueryResult
runningQuery = query
}
Self.healthStore.execute(query)
}
func stopLongRunningQuery() {
if let runningQuery = runningQuery {
Self.healthStore.stop(runningQuery)
self.runningQuery = nil
}
}
private func processQueryResult(_: HKAnchoredObjectQuery, samples: [HKSample]?, _: [HKDeletedObject]?, newAnchor: HKQueryAnchor?, error: Error?) {
guard let samples = samples as? [HKQuantitySample], error == nil else {
fatalError(error?.localizedDescription ?? "Failed to cast [HKSample] to [HKQuantitySample]")
}
previousAnchor = newAnchor
guard let sample = samples.last else {
return
}
DispatchQueue.main.async {
if Locale.current.usesMetricSystem {
let weight = sample.quantity.doubleValue(for: .gramUnit(with: .kilo))
self.bodyWeight = .init(value: weight, unit: UnitMass.kilograms)
} else {
let weight = sample.quantity.doubleValue(for: .pound())
self.bodyWeight = .init(value: weight, unit: UnitMass.pounds)
}
}
}
}
// MARK: - HealthKit Authorization
extension WeightProvider {
private static let typesToRead: Set<HKObjectType> = [
weightSampleType,
]
func authorize(completion: #escaping (Bool, Error?) -> Swift.Void) {
Self.healthStore.requestAuthorization(toShare: nil, read: Self.typesToRead) { success, error in
completion(success, error)
}
}
}
In my Views onAppear I call this function:
private func authorizeHealthKit() {
guard firstRun else {
return
}
firstRun = false
weightProvider.authorize { success, error in
guard success, error == nil else {
return
}
weightProvider.getBodyWeight(longRunning: true)
}
}
HealthKit is properly authorized as I can see in the Settings of my Watch. Any ideas? Any tips for my code in general?
Wow, after all this time I found the issue: The line previousAnchor = newAnchor needs to be after the guard statement. That's it.
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
Is it possible to read Apple Watch move goal from HealthKit?
I can retrieve the Move value by using the Quantity Identifier HKQuantityTypeIdentifier.activeEnergyBurned. I could not find a similar identifier for the move goal.
//Declared globally
var healthStore: HKHealthStore?
func prepareHealthKit() {
guard HKHealthStore.isHealthDataAvailable() else {
return
}
var readTypes = Set<HKObjectType>()
readTypes.insert(HKObjectType.activitySummaryType())
healthStore = HKHealthStore()
healthStore!.requestAuthorization(toShare: nil, read: readTypes) { (isSuccess, error) in
/*
Assuming you know the following steps:
1. Start workout session: i.e. "HKWorkoutSession"
2. Wait for delegate: i.e "workoutSession(_:didChangeTo:from:date:)"
3. Start Query for Activity Summary in the delegate:
i.e our "startQueryForActivitySummary()"
*/
}
}
func startQueryForActivitySummary() {
func createPredicate() -> NSPredicate? {
let calendar = Calendar.autoupdatingCurrent
var dateComponents = calendar.dateComponents([.year, .month, .day],
from: Date())
dateComponents.calendar = calendar
let predicate = HKQuery.predicateForActivitySummary(with: dateComponents)
return predicate
}
let queryPredicate = createPredicate()
let query = HKActivitySummaryQuery(predicate: queryPredicate) { (query, summaries, error) -> Void in
if let summaries = summaries {
for summary in summaries {
let activeEnergyBurned = summary.activeEnergyBurned.doubleValue(for: HKUnit.kilocalorie())
let activeEnergyBurnedGoal = summary.activeEnergyBurnedGoal.doubleValue(for: HKUnit.kilocalorie())
let activeEnergyBurnGoalPercent = round(activeEnergyBurned/activeEnergyBurnedGoal)
print(activeEnergyBurnGoalPercent)
}
}
}
healthStore?.execute(query)
}
References:
https://crunchybagel.com/accessing-activity-rings-data-from-healthkit
https://developer.apple.com/documentation/healthkit/hkactivitysummary
https://developer.apple.com/documentation/healthkit/hkactivitysummaryquery
I got the answer. The move goal is accessible from HKActivitySummary.
You should request permission to read HKActivitySummaryType:
let activitySummaryType = HKActivitySummaryType.activitySummaryType()
let readDataTypes: Set<HKObjectType> = [activitySummaryType]
healthStore.requestAuthorization(toShare: nil, read: readDataTypes, completion: myCompletionHandler)
Then use HKActivitySummaryQuery to read the summary information
let query = HKActivitySummaryQuery(predicate: myPredicate) { (query, summaries, error) -> Void in
if error != nil {
fatalError("*** Did not return a valid error object. ***")
}
if let activitySummaries = summaries {
for summary in activitySummaries {
print(summary.activeEnergyBurnedGoal)
//do something with the summary here...
}
}
}
healthStore.execute(query)
Other activity summary data that is accessible from HKActivitySummary is available here.
How can I add a second HKQuantityType to my health kit authorization?
I would like to add both the heart rate and the energy burned, as the array of QuantityTypes. When I add both of these types in the line ' let dataTypes = Set(arrayLiteral: quantityTypes)' instead of quantity type, as an array, I am getting an error "cannot convert value of HKQuantityType to expected argument type []
I am a beginner, I think I am not formatting the array correctly.
guard let heartRateQuantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned) else {
// displayNotAllowed()
return
}
guard let calorieQuantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned) else {
// displayNotAllowed()
return
}
let quantityTypes = [calorieQuantityType, heartRateQuantityType]
let dataTypes = Set(arrayLiteral: quantityTypes)
healthStore.requestAuthorizationToShareTypes(nil, readTypes: dataTypes) { (success, error) -> Void in
if success == false {
// self.displayNotAllowed()
}
}
you are setting the array to a Set then requesting authorisation. Instead add calorieQuantityType and heartRateQuantityType to the SET directly
//delete this let quantityTypes = [calorieQuantityType, heartRateQuantityType]
let dataTypes = Set([calorieQuantityType, heartRateQuantityType])
healthStore.requestAuthorizationToShareTypes(nil, readTypes: dataTypes) { (success, error) -> Void in
if success == false {
// self.displayNotAllowed()
}
}
I am using GCD in swift
like this :
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
//all background task
dispatch_async(dispatch_get_main_queue()) {
self.second()
}
}
In this code second function is getting called before completing all background task that's why I am not able to take some data which I am using in second function. I want to second method after completing all background task. Can anyone tell me how to achieve this task?
***************In background I am taking healthkit data like******
let healthKitTypesToRead =
Set(
arrayLiteral: HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierDateOfBirth)!,
HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierBiologicalSex)!,
HKObjectType.workoutType()
)
let newCompletion: ((Bool, NSError?) -> Void) = {
(success, error) -> Void in
if !success {
print("You didn't allow HealthKit to access these write data types.\nThe error was:\n \(error!.description).")
return
}
else
{
let stepCount = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
// Our search predicate which will fetch data from now until a day ago
// (Note, 1.day comes from an extension
// You'll want to change that to your own NSDate
//let date = NSDate()
//let predicate = HKQuery.predicateForSamplesWithStartDate(date, endDate: NSDate(), options: .None)
// The actual HealthKit Query which will fetch all of the steps and sub them up for us.
let stepCountQuery = HKSampleQuery(sampleType: stepCount!, predicate:.None, limit: 0, sortDescriptors: nil) { query, results, error in
var steps: Double = 0
if results?.count > 0
{
for result in results as! [HKQuantitySample]
{
steps += result.quantity.doubleValueForUnit(HKUnit.countUnit())
}
testClass.HK_stepCount = String(steps)
}
//completion(steps, error)
}
self.healthKitStore.executeQuery(stepCountQuery)
//EDIT.....
let tHeartRate = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
let tHeartRateQuery = HKSampleQuery(sampleType: tHeartRate!, predicate:.None, limit: 0, sortDescriptors: nil) { query, results, error in
if results?.count > 0
{
var string:String = ""
for result in results as! [HKQuantitySample]
{
let HeartRate = result.quantity
string = "\(HeartRate)"
print(string)
}
testClass.HK_HeartRate = string
finalCompletion(Success: true)
}
}
self.healthKitStore.executeQuery(tHeartRateQuery)
}
}
healthKitStore.requestAuthorizationToShareTypes(healthKitTypesToWrite, readTypes: healthKitTypesToRead, completion: newCompletion)
I am not able to take value of step count, It get executed after calling second(method) called, plz suggest me what to do?
You can create a separate function to execute your task on other thread
func someFunction(finalCompletion: (Success: Bool)->()) {
let healthKitTypesToRead =
Set(
arrayLiteral: HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierDateOfBirth)!,
HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierBiologicalSex)!,
HKObjectType.workoutType()
)
let newCompletion: ((Bool, NSError?) -> Void) = {
(success, error) -> Void in
if !success {
print("You didn't allow HealthKit to access these write data types.\nThe error was:\n \(error!.description).")
return
}
else
{
let stepCount = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
// Our search predicate which will fetch data from now until a day ago
// (Note, 1.day comes from an extension
// You'll want to change that to your own NSDate
//let date = NSDate()
//let predicate = HKQuery.predicateForSamplesWithStartDate(date, endDate: NSDate(), options: .None)
// The actual HealthKit Query which will fetch all of the steps and sub them up for us.
let stepCountQuery = HKSampleQuery(sampleType: stepCount!, predicate:.None, limit: 0, sortDescriptors: nil) { query, results, error in
var steps: Double = 0
if results?.count > 0
{
// Edit--
for result in results as! [HKQuantitySample]
{
steps += result.quantity.doubleValueForUnit(HKUnit.countUnit())
// heartBeat += ....
}
testClass.HK_stepCount = String(steps)
finalCompletion(Success: true)
}
//completion(steps, error)
}
self.healthKitStore.executeQuery(stepCountQuery)
}
}
healthKitStore.requestAuthorizationToShareTypes(healthKitTypesToWrite, readTypes: healthKitTypesToRead, completion: newCompletion)
}
Another function?
I will edit this answer in some time to tell you about a better technique to deal with async request. In general you should have a separate singleton class for such background tasks. (RESTful API service class.. but for now you can use the below method)
func getHeartBeatInfo(finalCompletionHeart: (Success: Bool)->()) {
let tHeartRate = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeartRate)
let tHeartRateQuery = HKSampleQuery(sampleType: tHeartRate!, predicate:.None, limit: 0, sortDescriptors: nil) { query, results, error in
if results?.count > 0
{
var string:String = ""
for result in results as! [HKQuantitySample]
{
let HeartRate = result.quantity
string = "\(HeartRate)"
print(string)
}
testClass.HK_HeartRate = string
finalCompletionHeart(Success: true)
}
}
self.healthKitStore.executeQuery(tHeartRateQuery)
}
And then you can call this method like so:
override func viewDidLoad() {
someFunction( { (finalCompletion) in
if finalCompletion == true {
getHeartBeatInfo( { finalCompletionHeart in
if finalCompletionHeart == true {
self.second()
}
})
}
})
}