iOS HealthKit Project using HKActivitySummaryQuery - Summaries Always Nil - ios

Objective: Read from HealthKit My Activity Data ( i.e. activity ring data - move calories, workout minutes, and stands )
Problem Experienced: Result Handler returns 'nil' HKActivitySummary Array
What I have Attempted:
I have added the HealthKit Capability to the project and added the two info.plist requirements:
Privacy - Health Share Usage Description
Privacy - Health Update Usage Description
And ensured I tapped "allow all" for permissions when the permission request popup appears for HealthKit within the iPhone.
I also have many weeks of activity data recorded so that should also be fine.
How can I retrieve the array of HKActivitySummary objects in order to replicate the rings within my iOS app.
My Code:
override func viewDidLoad() {
super.viewDidLoad()
startQueryForActivitySummary(view: self.view)
}
func startQueryForActivitySummary(view: UIView) {
let calendar = NSCalendar.current
let endDate = Date()
guard let startDate = calendar.date(byAdding: .day, value: -7, to: endDate) else {
fatalError("*** Unable to create the start date ***")
}
let units: Set<Calendar.Component> = [.day, .month, .year, .era]
var startDateComponents = calendar.dateComponents(units, from: startDate)
startDateComponents.calendar = calendar
var endDateComponents = calendar.dateComponents(units, from: endDate)
endDateComponents.calendar = calendar
let queryPredicate = HKQuery.predicate(forActivitySummariesBetweenStart: startDateComponents,
end: endDateComponents)
let query = HKActivitySummaryQuery(predicate: queryPredicate) { (query, summaries, error) -> Void in
if let summaries = summaries { // print(summaries) before this line will always return nil.
if let summary = summaries.first {
let activeEnergyBurned = summary.activeEnergyBurned.doubleValue(for: HKUnit.kilocalorie())
let activeEnergyBurnedGoal = summary.activeEnergyBurnedGoal.doubleValue(for: HKUnit.kilocalorie())
let activeEnergyBurnGoalPercent = round(activeEnergyBurned/activeEnergyBurnedGoal)
let frame = CGRect(x: 0, y: 0, width: 200, height: 200)
let ringView = HKActivityRingView(frame: frame)
view.addSubview(ringView)
ringView.setActivitySummary(summary, animated: true)
}
}
}
let allTypes = Set([HKObjectType.workoutType(),
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!])
let healthStore = HKHealthStore()
healthStore.requestAuthorization(toShare: allTypes, read: allTypes) { (success, error) in
healthStore.execute(query)
}
}

I think you are missing permission HKObjectType.activitySummaryType()
Extend you types like this:
let allTypes = Set([
HKObjectType.workoutType(),
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.activitySummaryType()
])
Here is the documentation: https://developer.apple.com/documentation/healthkit/hkobjecttype/1615319-activitysummarytype

Related

Swift 2d array empty after HealthKit function call - thread issues

I'm populating a twodimensional array with HealthKit data. It's working fine as long as the function loops through the 30 days. outside of that function the array is empty because HealthKit data is loaded in background and everything else executes before.
How can I wait until the data is loaded so I can access the data inside the arrays?
Here's what's happening inside my ViewController
print("function call starting") // 1
// Populate 2DArrays
self.hkManager.getTotalCalories(forDay: 30) {caloriesValue, date in
//check if caloriesValue is nil, only do something if it's not
guard let caloriesValue = caloriesValue else { return }
let dateValue = DateFormatter.localizedString(from: date!, dateStyle: .short, timeStyle: .none)
print("will append") // 7 (31x)
self.caloriesArray.append([(dateValue,caloriesValue)])
}
print("function call done") // 2
print(self.caloriesArray.count) // 3
hkManager.getWeightData(forDay: 30) {bodyMassValue, date in
// Check if bodyMassValue is nil, only do something, if it's not
guard let bodyMassValue = bodyMassValue else { return }
let dateValue = DateFormatter.localizedString(from: date!, dateStyle: .short, timeStyle: .none)
self.bodyMassArray.append([(dateValue,bodyMassValue)])
}
do {
self.age = try hkManager.getAge()
} catch let error {
print("Error calculating age: \(error)")
}
print(bodyMassArray) // 4
print(caloriesArray) // 5
print(age!) // 6
}
I've added numbers behind the print statements to make clear what get's executed when.
The functions I'm calling look like this:
func getTotalCalories(forDay days: Int, completion: #escaping ((_ calories: Int?, _ date: Date?) -> Void)) {
// Getting quantityType as .dietaryCaloriesConsumed
guard let calories = HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed) else {
print("*** Unable to create a dietaryEnergyConsumed type ***")
return
}
let now = Date()
let startDate = Calendar.current.date(byAdding: DateComponents(day: -days), to: now)!
var interval = DateComponents()
interval.day = 1
var anchorComponents = Calendar.current.dateComponents([.day, .month, .year], from: now)
anchorComponents.hour = 0
let anchorDate = Calendar.current.date(from: anchorComponents)!
let query = HKStatisticsCollectionQuery(quantityType: calories,
quantitySamplePredicate: nil,
options: [.cumulativeSum],
anchorDate: anchorDate,
intervalComponents: interval)
query.initialResultsHandler = { _, results, error in
guard let results = results else {
print("ERROR")
return
}
results.enumerateStatistics(from: startDate, to: now) { statistics, _ in
DispatchQueue.main.async {
if let sum = statistics.sumQuantity() {
let calories = Int(sum.doubleValue(for: HKUnit.kilocalorie()).rounded())
completion(calories, statistics.startDate)
return
}
}
}
}
healthStore.execute(query)
}
Anyone any ideas what I need to do?
Thanks! :)

Swift - Exercise minutes Healthkit

So I have been trying to get the exercise minutes from Healthkit onto my application and store it in a variable.
But the whenever the application is opened on an Iphone connected to an apple wathc, it crashes. I have tried debugging the application, but it works fine on a simulator or my Ipod touch. This the function I am using to retrieve the exercise minutes.
func getExerciseTime(completion: #escaping (Double) -> Void) {
let exerciseQuantityType = HKQuantityType.quantityType(forIdentifier: .appleExerciseTime)!
/*
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
*/
var now : Date
var startOfDay : Date
var predicate : NSPredicate
switch dwmValue {
case 0:
now = Date()
startOfDay = Calendar.current.startOfDay(for: now)
predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
break
case 1:
now = Date()
startOfDay = Calendar.current.startOfDay(for: Date(timeIntervalSinceNow: -60 * 60 * 24 * 7))
predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
break
case 2:
now = Date()
let wx = -60 * 60 * 24 * 2
startOfDay = Calendar.current.startOfDay(for: Date(timeIntervalSinceNow: TimeInterval((-60 * 60 * 24 * 7 * 4) + wx)))
predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
break
default:
now = Date()
startOfDay = Calendar.current.startOfDay(for: now)
predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
break
}
let query = HKStatisticsQuery(quantityType: exerciseQuantityType, quantitySamplePredicate: predicate, options: .cumulativeSum ) { (_, result, error) in
var resultCount = 0.0
guard let result = result else {
//log.error("Failed to fetch steps = \(error?.localizedDescription ?? "N/A")")
completion(resultCount)
return
}
if let sum = result.sumQuantity() {
resultCount = sum.doubleValue(for: HKUnit.count())
}
DispatchQueue.main.async {
completion(resultCount)
print("Exercise time : \(resultCount)")
}
}
healthKitStore.execute(query)
}
This is the code I use in viewdidAppear to store the value from the above function in a global variable
getExerciseTime(){ time in
exerciseTime = time
}
I have no idea why the application keeps crashing on the Iphone. I have tried to change the options in StatisticsQuery but nothing has worked. Please help me out here!! And I know there is no problem with healthkit authentication as it return some data on the simulator and the iPod but crashes on the Iphone that is connected to an apple watch.
When you are summing the quantities, you are using an incompatible type (HKUnit.count()), you need to use a time unit.
resultCount = sum.doubleValue(for: HKUnit.minute())
Also, if you are not doing so already, you need to ask for permission to read
override func viewDidAppear(_ animated: Bool) {
healthKitStore.requestAuthorization(toShare: nil, read: [exerciseQuantityType], completion: { (userWasShownPermissionView, error) in
self.getExerciseTime(){ time in
self.exerciseTime = time
}
})
}
You need to set a usage description in your plist
<key>NSHealthShareUsageDescription</key>
<string>Foo</string>
Aslo your app needs HealthKit capability set in the project target settings.

Can not fetch the updating data from CoreData after saving

I refer the programming guide to save data to CoreData, and My codegen of Entity is Category/Extension(pic)
I use the performBackgroundTask to save the data.
While, I can not fetch the data after saving.
I have to force close an app and then reopen it again, and the I can fetch the saving data.
The followings are my code.
The method to save the data to CoreData:
var container: NSPersistentContainer? =
(UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
private func updateDatabase() {
container?.performBackgroundTask { [weak self] context in
if let name = self?.employeeName.text {
_ = try? Employee.findOrCreateEmployee(matching: name, in: context)
}
var date = (self?.startDate)!
let end = (self?.endDate)!
let fmt = DateFormatter()
fmt.dateFormat = "MM/dd"
while date <= end {
//print(fmt.string(from: date))
_ = try? Shift.findOrCreateShift(byEmployee: (self?.employeeName1.text)!, date: fmt.string(from: date), assignment: nil, in: context)
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
try? context.save()
}
}
The method to fetch the data from CoreData:
var container: NSPersistentContainer? =
(UIApplication.shared.delegate as? AppDelegate)?.persistentContainer {
didSet { updateUI() }
}
private func updateUI() {
if let context = container?.viewContext {
let employeeRequest: NSFetchRequest<Employee> = Employee.fetchRequest()
employeeRequest.sortDescriptors = [NSSortDescriptor(
key: "created",
ascending: true,
)]
employees = try? context.fetch(employeeRequest)
let shiftsRequest: NSFetchRequest<Shift> = Shift.fetchRequest()
shiftsRequest.sortDescriptors = [ NSSortDescriptor(
key: "date",
ascending:true
)]
shifts = try? context.fetch(shiftsRequest)
}
}
Does anyone has any suggestion?
Thanks.

Getting yesterdays steps from HealthKit

I'm building an app for personal use, and I am currently stuck on how to accurately get yesterdays steps from the healthkit. And then from there, placing it into a variable (should be easy, I know).
I have a HealthKitManager class that calls the function from inside a view, and then appends that to a variable from that same view.
I have scoured most of the healthKit questions, and I get back data, but I don't think it is accurate data. My phone data from yesterday is 1442 steps, but it's returning 2665 steps. On top of that, when I try to put the data is a variable it prints out as 0.
HealthKitManagerClass
import Foundation
import HealthKit
class HealthKitManager {
let storage = HKHealthStore()
init()
{
checkAuthorization()
}
func checkAuthorization() -> Bool
{
// Default to assuming that we're authorized
var isEnabled = true
// Do we have access to HealthKit on this device?
if HKHealthStore.isHealthDataAvailable()
{
// We have to request each data type explicitly
let steps = NSSet(object: HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!)
// Now we can request authorization for step count data
storage.requestAuthorizationToShareTypes(nil, readTypes: steps as? Set<HKObjectType>) { (success, error) -> Void in
isEnabled = success
}
}
else
{
isEnabled = false
}
return isEnabled
}
func yesterdaySteps(completion: (Double, NSError?) -> ())
{
// The type of data we are requesting (this is redundant and could probably be an enumeration
let type = 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 calendar = NSCalendar.currentCalendar()
let yesterday = calendar.dateByAddingUnit(.Day, value: -1, toDate: NSDate(), options: [])
//this is probably why my data is wrong
let predicate = HKQuery.predicateForSamplesWithStartDate(yesterday, endDate: NSDate(), options: .None)
// The actual HealthKit Query which will fetch all of the steps and sub them up for us.
let query = HKSampleQuery(sampleType: type!, predicate: predicate, 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())
}
}
//I'm unsure if this is correct as well
completion(steps, error)
print("\(steps) STEPS FROM HEALTH KIT")
//this adds the steps to my character (is this in the right place)
Player.User.Gold.addSteps(Int(steps))
}
//not 100% on what this does, but I know it is necessary
storage.executeQuery(query)
}}
ViewControllerClass
import UIKit
import Foundation
class UpdateViewController: UIViewController {
#IBOutlet var back: UIButton!
let HKM = HealthKitManager()
var stepsFromPhone = Double()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
back.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
HKM.yesterdaySteps(){ steps, error in
self.stepsFromPhone = steps
}
Player.User.Gold.addSteps(Int(stepsFromPhone))
print(Player.User.Gold.getSteps(), "IN PLAYER")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
The output from
print(Player.User.Gold.getSteps(), "IN PLAYER")
is
0 IN PLAYER
The output from
print("\(steps) STEPS FROM HEALTH KIT")
is
2665.0 STEPS FROM HEALTH KIT
so, basically my questions are:
what NSDate() do I need for the whole of yesterday?
how do I take the steps from the yesterdaySteps() and correctly place them into a variable in the UpdateViewController?
Thank you for any help!
This is the method I am using in my healthStore class
func TodayTotalSteps(completion: (stepRetrieved: Double) -> Void) {
let type = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount) // The type of data we are requesting
let calendar = NSCalendar.currentCalendar()
let interval = NSDateComponents()
interval.day = 1
let anchorComponents = calendar.components([.Day , .Month , .Year], fromDate: NSDate())
anchorComponents.hour = 0
let anchorDate = calendar.dateFromComponents(anchorComponents)
let stepsQuery = HKStatisticsCollectionQuery(quantityType: type!, quantitySamplePredicate: nil, options: .CumulativeSum, anchorDate: anchorDate!, intervalComponents: interval)
stepsQuery.initialResultsHandler = {query, results, error in
let endDate = NSDate()
var steps = 0.0
let startDate = calendar.dateByAddingUnit(.Day, value: 0, toDate: endDate, options: [])
if let myResults = results{ myResults.enumerateStatisticsFromDate(startDate!, toDate: endDate) { statistics, stop in
if let quantity = statistics.sumQuantity(){
let date = statistics.startDate
steps = quantity.doubleValueForUnit(HKUnit.countUnit())
// print("\(date): steps = \(steps)")
}
completion(stepRetrieved: steps)
}
} else {
completion(stepRetrieved: steps)
}
}
executeQuery(stepsQuery)
}
and here is How I am using it
func getStepsData() {
// I am sendng steps to my server thats why using this variable
var stepsToSend = 0
MyHealthStore.sharedHealthStore.todayManuallyAddedSteps({ (steps , error) in
if error != nil{
// handle error
}
else{
// truncating manuall steps
MyHealthStore.sharedHealthStore.TodayTotalSteps({ (stepRetrieved) in
stepsToSend = Int(stepRetrieved - steps)
})
}
})
}
and here is the function used above for manually added steps which we are truncating in order to get exact steps
func todayManuallyAddedSteps(completion: (Double, NSError?) -> () )
{
let type = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount) // The type of data we are requesting
let date = NSDate()
let cal = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let newDate = cal.startOfDayForDate(date)
let predicate = HKQuery.predicateForSamplesWithStartDate(newDate, endDate: NSDate(), options: .None) // Our search predicate which will fetch all steps taken today
// The actual HealthKit Query which will fetch all of the steps and add them up for us.
let query = HKSampleQuery(sampleType: type!, predicate: predicate, limit: 0, sortDescriptors: nil) { query, results, error in
var steps: Double = 0
if results?.count > 0
{
for result in results as! [HKQuantitySample]
{
// checking and adding manually added steps
if result.sourceRevision.source.name == "Health" {
// these are manually added steps
steps += result.quantity.doubleValueForUnit(HKUnit.countUnit())
}
else{
// these are auto detected steps which we do not want from using HKSampleQuery
}
}
completion(steps, error)
} else {
completion(steps, error)
}
}
executeQuery(query)
}
I hope it helps. Let me know if you face any issue.
You can use HKStatisticsQuery
let quantityType = HKSampleType.quantityType(forIdentifier: .stepCount)!
let predicate = HKQuery.predicateForSamples(
withStart: startDate,
end: endDate,
options: [.strictStartDate, .strictEndDate]
)
let query = HKStatisticsQuery(
quantityType: quantityType,
quantitySamplePredicate: predicate,
options: .cumulativeSum) { (query, result, error) in
guard let result = result, error == nil else {
print("HeathService error \(String(describing: error))")
return
}
callback(result)
}

CMPedometer in iOS 10

I'm trying to access CMPedometer data in iOS 10 and I've followed a few tutorials and I just can't seem to get anything to work. I've never tried to access this information before so don't really know where to start. I'd love some help on getting something setup. Any help you can provide will be greatly appreciated.
I want to do both live updates and also access information form the passed but only within the same day.
This currently doesn't open and prints a massive error.
import UIKit
import CoreMotion
class ViewController: UIViewController {
var days:[String] = []
var stepsTaken:[Int] = []
let activityManager = CMMotionActivityManager()
let pedoMeter = CMPedometer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let cal = Calendar.current
var comps = cal.components([.year, .month, .day, .hour, .minute, .second], from: Date())
comps.hour = 0
comps.minute = 0
comps.second = 0
let timeZone = TimeZone.system
cal.timeZone = timeZone
let midnightOfToday = cal.date(from: comps)!
if(CMPedometer.isStepCountingAvailable()){
self.pedoMeter.startUpdates(from: midnightOfToday) { (data: CMPedometerData?, error) -> Void in
DispatchQueue.main.async(execute: { () -> Void in
if(error == nil){
print("\(data!.numberOfSteps)")
//self.step.text = "\(data!.numberOfSteps)"
}
})
}
}
}
}
I've been playing around with some things and realised that if the simulator is open as well a big error message appears. However if its closed and running only on my phone the error is:
2016-07-16 18:13:38.054710 test[419:37870] [access] private
You are using a beta version of Xcode, the log is common for all users. Nothing to worry about it. Edit your code lil bit.
import UIKit
import CoreMotion
class ViewController: UIViewController {
var days:[String] = []
var stepsTaken:[Int] = []
let activityManager = CMMotionActivityManager()
let pedoMeter = CMPedometer()
override func viewDidLoad() {
super.viewDidLoad()
let cal = Calendar.current
var comps = cal.components([.year, .month, .day, .hour, .minute, .second], from: Date())
comps.hour = 0
comps.minute = 0
comps.second = 0
let timeZone = TimeZone.system
cal.timeZone = timeZone
let midnightOfToday = cal.date(from: comps)!
#if arch(i386) || arch(x86_64) && os(iOS)
// Simulator
#else
// Run only in Physical Device, iOS
if(CMPedometer.isStepCountingAvailable()){
self.pedoMeter.startUpdates(from: midnightOfToday) { (data: CMPedometerData?, error) -> Void in
DispatchQueue.main.async(execute: { () -> Void in
if(error == nil){
print("\(data!.numberOfSteps)")
//self.step.text = "\(data!.numberOfSteps)"
}
})
}
}
#endif
}
}
Add key Privacy - Motion Usage Description in info.plist, with String value YES.
Try running the app again in iPhone and check.

Resources