HealthKit for iOS8: Part 2

So you got past the intro and learned about the health store and how we need authorization from the user in order to have read and write access to certain properties.  Now that the user trusts your app, let’s learn how to interact with the health store.

1. HKObjects and the health store

Before we get into reading and writing, let’s get to know HKObjects.  Here is the class tree:

HealthKit HKObject Hierarchy Santiapps.com
HealthKit HKObject Hierarchy

As you can see, HKObjectType inherits directly from NSObject.  You can basically have 2 types of HKObjects; HKCharacteristic and HKSample -Type.  HKCharacteristicType doesn’t change over time whereas HKSampleType does change, such as calories consumed, calories burnt, Glucose levels etc.  Let’s see some examples:

HKCharactersiticType has 3 main objects:

Biological Sex

Blood Type

Birthdate.

HKSampleType has objects like:

-HKCategoryType: discrete, finite values that can be enumerated such as sleep analysis.

-HKCorrelationType: for creating correlated objects grouped as one.

-HKQuantityType: used for creating objects that store numerical values.

-HKWorkoutType: used for creating workout objects.

-FURTHERMORE: HKSample has HKCategorySample & HKQuantitySample

For the most part you will simply fetch HKCharacteristic types from the health store of the user.  The user will have input those values through his/her Health app.  HKSample types on the other hand you will read and write constantly.  Let’s think about how we need to write data:

A. Decide on a Type Identifier

B. Create a matching HKObjectType

C. Create an HKSample

D. Call saveObject on the health store

Let’s take a look at saving data:

self.healthStore?.saveObject(calorieSample, withCompletion: { (success, error) in
// do a bunch of UI stuff probably in the main queue
})

Ok so you might be thinking, what kinds of objects are involved here.  What is dateOfBirth or calorieSample?  Well the best way of understanding is to see an example.  Here are images representing the logic behind writing data to the store.  As mentioned earlier, you find a matching type, set it, create a sample for it and saveObject!  This is the process for a Category type:

HealthKit iOS8 HKCategory by Santiapps.com
HealthKit iOS8 HKCategory

We need to find the matching identifier for the category type in order to save a Category Sample.  Likewise for the Quantity type:

HealthKit iOS8 HKQuantity by Santiapps.com
HealthKit iOS8 HKQuantity

What this means is that before you call saveObject and pass it in that calorieSample, you need to have properly defined calorieSample:

// 1.  MUST FIND THE RIGHT TYPE FOR ENERGY BEING CONSUMED

var quantityType: HKQuantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDietaryEnergyConsumed)

// 2. MUST CREATE AND HKQUANTITY FOR THAT TYPE

var quantity: HKQuantity = HKQuantity(unit: HKUnit.jouleUnit(), doubleValue:300)

// 3. MUST DEFINE A DATE FOR THE SAMPLE AND OPTIONALLY SOME METADATA

var now: NSDate = NSDate()

var metadata: NSDictionary = ["HKMetadataKeyFoodType":"Ham Sandwich"]

//4. CREATE HKOBJECT TO BE SAVED

var calorieSample: HKQuantitySample = HKQuantitySample(type: quantityType, quantity:quantity, startDate:now, endDate:now, metadata:metadata)

Finally you can call the saveObject method.  That’s not so bad.  So you would do the same for energy burnt in a workout, calcium intake, etc.

Now let’s take a look at how we would read data from the store:

func fetchUsersAge () -> () {

var error: NSError?

var dateOfBirth = self.healthStore?.dateOfBirthWithError(&error)

if error != nil {

NSLog("An error occured fetching the user's age information. In your app, try to handle this gracefully. The error was: \(error)")

//Present VC

abort()

}

}

As you can see, this is quite simple, we basically say self.healthStore?.dateOfBirthWithError.
Now dateOfBirth is not only a finite, or discrete measure but its also a Characteristic type, it doesn’t change with time.  How would you read other data, HKSampleTypes such as height and weight or Dietary Calories which are not discrete, in other words not whole numbers but infinitesimally scalar and change constantly?  That’s what HKQuery is for!

2. Queries & Stats

Sometimes you want to be able to query the health store for data, that’s what HKQuery is for.  If it’s a quantity sample type then we probably want to save the start and end date with it as before.  So here is an example of how to query our health store:

// 1. Create a date

var now: NSDate = NSDate()

let calendar : NSCalendar = NSCalendar.currentCalendar()

var components: NSDateComponents = calendar.components(.CalendarUnitYear | .CalendarUnitMonth | .CalendarUnitDay, fromDate: now)

var startDate: NSDate = calendar.dateFromComponents(components)!

var endDate: NSDate = calendar.dateByAddingUnit(.CalendarUnitDay, value:1, toDate:startDate, options:nil)!

// 2. Create the identifier

var sampleType: HKSampleType = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDietaryEnergyConsumed)

// 3. Create the predicate to search with

var predicate: NSPredicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate:endDate, options:.None)

// 4. Create the HKSampleQuery because we are querying sample types

var query: HKSampleQuery = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: 0, sortDescriptors: nil) { (query:HKSampleQuery?, results:[AnyObject]!, error:NSError!) -> Void in

if (error != nil) {

NSLog("An error occured fetching the user's tracked food. In your app, try to handle this gracefully. The error was: %@.", error)

abort()

}

if results != nil {

NSLog("Got something!")

}

// do UI stuff probably in the main queue

self.healthStore?.executeQuery(query)     // EXECUTE QUERY

}

3. User input UI

So let’s start creating an app.  As you can see, this is all based on the sample app from Apple called Fit.  But it has been modified to add some workout data.  What we want is to be able to read data from our health store (which will provide us with a user’s profile view), collect some user info on how much energy was consumed (which will give us our Energy Consumption view) and have the user input their workout info which in our case is going to be a swimming app (this will give us a Workout view).  For the last 2 data, we will write to the health store how much energy we consumed and how much we burnt.

Ok so we have our AppDelegate and Storyboard.  To recap, you’ve activated the HealthKit capabilities in your project which if you go to the Dev Center, Xcode has created a Team provisioning profile for this app you created and its enabled for HealthKit.  This also gave you an Entitlements file in your navigator which looks like this:

HealthKit app iOS8 by Santiapps.com
HealthKit app iOS8

The Journal file is something I create to keep track of my work, ignore that.  The FoodItem is a file we will quickly throw up on the screen but is a simple Custom Class file to be used in the UI part of this app.  For the first part which deals with the Profile view controller we won’t concern ourselves with it.

Ok so let’s take a quick look at our Profile view controller class.  As always we start out with our properties:

class ProfileViewController: UITableViewController, UITextFieldDelegate {
@IBOutlet var ageValueLabel: UITextField!
@IBOutlet var ageUnitLabel: UILabel?
@IBOutlet var heightValueTextField: UITextField!
@IBOutlet var heightUnitLabel: UILabel?
@IBOutlet var weightValueTextField: UITextField!
@IBOutlet var weightUnitLabel: UILabel?
var healthStore: HKHealthStore?
}

We have created 6 IBOutlet’s.  Each UILabel will hold unit values and each corresponding UITextField will hold the respective data we fetch from the health store.

Then we create a variable for our HealthStore which, if you recall, is set for us from our AppDelegate once it receives authorization.

Now our methods:

Each time our view will appear, we want to update our data, since it may have changed:

override func viewWillAppear(animated: Bool) {
// Update the user interface based on the current user's health information.
self.updateUsersAge()
self.updateUsersHeight()
self.updateUsersWeight()
}

Ok so with each viewWillAppear, we call methods to update our UI with the latest data from our store, which of course means we will fetch from the store and as you recall, it was different fetching a CharacteristicType such as DOB vs a Sample Type such as weight.  So let’s take a look at the simplest one first:

func updateUsersAge () -> () {

var error: NSError?

var dateOfBirth = self.healthStore?.dateOfBirthWithError(&error)

if error != nil {

NSLog("An error occured fetching the user's age information. In your app, try to handle this gracefully. The error was: \(error)")

abort()

}

if dateOfBirth != nil {

NSLog("Found a DOB \(dateOfBirth)")

}

if dateOfBirth == nil {

var dateFormatter = NSDateFormatter()

dateFormatter.dateFormat = "yyyy-MM-dd"

dateOfBirth = dateFormatter.dateFromString("1974-10-18")!

}

// Compute the age of the user.

let now: NSDate = NSDate()

let calendar : NSCalendar = NSCalendar.currentCalendar()

var ageComponents = calendar.components(.CalendarUnitYear,

fromDate: dateOfBirth!,

toDate: now,

options:nil)

var usersAge: Int = ageComponents.year

var ageValueString: NSString = NSNumberFormatter.localizedStringFromNumber(usersAge, numberStyle:NSNumberFormatterStyle.NoStyle)

if let something = self.ageValueLabel!.text {

self.ageValueLabel!.text = ageValueString

}

}

Ok so first we simply fetch the value for dateOfBirth from our store and if we get an error back, we log it.  If dateOfBirth is not nil, then we log success.  You will see a strange bit now, where it checks whether if DOB is nil.  I know, we already checked if error was nil, so DOB should not be nil, but sometimes its empty.  The user may not have set DOB on his or her device yet.  This means you will get nil and your app will crash.  So in case DOB IS nil, we hard code one.  Finally we compute the user’s age based on their DOB by using NSDateComponents and we assign the computed Int to our label using NSNumberFormatter.localizedStringFromNumber method.

Now let’s see how to get height and weight, which are Sample types:

func updateUsersHeight () -> () {

// Fetch user's default height unit in inches.

var lengthFormatter: NSLengthFormatter = NSLengthFormatter()

lengthFormatter.unitStyle = NSFormattingUnitStyle.Long

var heightFormatterUnit: NSLengthFormatterUnit = NSLengthFormatterUnit.Inch

self.heightUnitLabel!.text = lengthFormatter.unitStringFromValue(10, unit:heightFormatterUnit)

var heightType: HKQuantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)

self.fetchMostRecentDataOfQuantityType(heightType, withCompletion: { (mostRecentQuantity:HKQuantity?, error:NSError?) -> () in

//some code

if let something = error {

NSLog("An error occured fetching the user's height information. In your app, try to handle this gracefully. The error was: %@.", something)

abort()

}

//Determine the height in the required unit.

var usersHeight: Double = 0.0

if let somethingElse = mostRecentQuantity {

var heightUnit: HKUnit = HKUnit.inchUnit()

usersHeight = mostRecentQuantity!.doubleValueForUnit(heightUnit)

// Update the user interface.

dispatch_async(dispatch_get_main_queue(), {

self.heightValueTextField.text = NSNumberFormatter.localizedStringFromNumber(usersHeight, numberStyle:.NoStyle)

})

}

})

}

Ok so we use NSLengthFormatter to determine the length style to use.  Then we use its NSLengthFormatterUnit in order to specify the unit in order to set the label for the height value.  Now we get into fetching requirements, so we must create the matching type first and then pass it into our fetch method which is called fetchMostRecentDataOfQuantityType.  So we are passing our quantity type of height, to this method.  We will study that method in a bit, but for the time being, notice we pass in a completion handler.  If that completion handler receives an error, it will log the error.  Otherwise, we create a variable for height, use the received mostRecentQuantity variable and test it since its an optional, create a heightUnit from HKUnit and unwrap the mostRecentQuantity value into our usersHeight variable using our heightUnit.

What?  Double take!  Ok, here we go, this time with weight:

func updateUsersWeight () -> (){

// Fetch the user's default weight unit in pounds.

var massFormatter: NSMassFormatter = NSMassFormatter()

massFormatter.unitStyle = NSFormattingUnitStyle.Long

var weightFormatterUnit: NSMassFormatterUnit = NSMassFormatterUnit.Pound

self.weightUnitLabel!.text = massFormatter.unitStringFromValue(10, unit:weightFormatterUnit)

// Query to get the user's latest weight, if it exists.

var weightType: HKQuantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass)

self.fetchMostRecentDataOfQuantityType(weightType, withCompletion: { (mostRecentQuantity: HKQuantity?, error: NSError?) -> () in

if ((error) != nil) {

NSLog("An error occured fetching the user's weight information. In your app, try to handle this gracefully. The error was: %@.", error!);

abort()

}

// Determine the weight in the required unit.

var usersWeight:Double = 0.0

if let somethingArse = mostRecentQuantity {

var weightUnit: HKUnit = HKUnit.poundUnit()

usersWeight = mostRecentQuantity!.doubleValueForUnit(weightUnit)

// Update the user interface.

dispatch_async(dispatch_get_main_queue(), {

self.weightValueTextField.text = NSNumberFormatter.localizedStringFromNumber(usersWeight, numberStyle:.NoStyle)

})

}

})

}

Again, we use NSMassFormatter for our style, its NSMassFormatterUnit for our unit, create the weight type identifier and pass it in to our fetch method along with a completion handler.  If the completion handler returns error, we log it.  Otherwise, we create a local variable to store the usersWeight, if let check mostRecentQuantity because its an optional and use it to assign its doubleValue using weightUnit to our local usersWeight variable.  Finally we update the UI in the main queue as always.

Ok so let’s take a look at this magical method, fetch something or other:

func fetchMostRecentDataOfQuantityType(quantityType: HKQuantityType, withCompletion completion: ((mostRecentQuantity:HKQuantity?, error:NSError?) -> ())? ) {

let timeSortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)

let query = HKSampleQuery(sampleType: quantityType, predicate: nil, limit: 1, sortDescriptors: [timeSortDescriptor]) { query, results, error in

if completion != nil && error != nil {

completion!(mostRecentQuantity: nil, error: error)

return;

}

let resultsArray = results as NSArray?

var quantitySample: HKQuantitySample? = resultsArray?.firstObject as HKQuantitySample?

var quantity: HKQuantity? = quantitySample?.quantity

if completion != nil {

completion!(mostRecentQuantity: quantity, error: error)

}

}

self.healthStore?.executeQuery(query)

}

Ok, not that complicated really.  We create a NSSortDescriptor in order to arrange our results and be able to get the latest one.  We create the HKQuery and pass it in the sampleType (height or weight, depending on what was passed into the fetch method), limit results to 1 and no predicate.  We pass in the sort descriptor and another completion handler.  Here are the cases tested next:

– If the HKQuery completion handler is NOT nil && the error is also NOT nil, then there is an error, so return the other completion handler with nil

– Otherwise, take the results AnyObject from the HKQuery and cast it as NSArray, get its firstObject as HKQuantitySample, get its quantity and set it to some local variable called quantity.  Then again check if the original completion is NOT nil, then return the original completion handler with the quantity results.  Finally execute the query!

Finally, you may have noticed the user can update their height and weight.  And we adopted the UITextFieldDelegate protocol for that.  So let’s:

func textFieldShouldReturn (textField: UITextField) -> (ObjCBool) {

textField.resignFirstResponder()

if textField == self.heightValueTextField {

self.saveHeightIntoHealthStore()

} else if textField == self.weightValueTextField {

self.saveWeightIntoHealthStore()

}

return true;

}

call save each time the user modifies their height or weight.  And those methods are simply:

func saveHeightIntoHealthStore () -> () {

var formatter: NSNumberFormatter = self.numberFormatter()

var height: NSNumber?

if let somethingA = self.heightValueTextField!.text {

height = formatter.numberFromString(self.heightValueTextField!.text)

}

let cosa:NSNumber = 8

var otra = cosa.doubleValue

if height == height {

// Save the user's height into HealthKit.

var heightType: HKQuantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)

var heightQuantity: HKQuantity = HKQuantity(unit:HKUnit.inchUnit(), doubleValue: height!.doubleValue)

var heightSample: HKQuantitySample = HKQuantitySample(type: heightType, quantity:heightQuantity, startDate:NSDate(), endDate:NSDate())

self.healthStore?.saveObject(heightSample, withCompletion: { (success, error) in

if (!success) {

NSLog("An error occured saving the height sample %@. In your app, try to handle this gracefully. The error was: %@.", heightSample, error)

abort()

}

})

}

}

Format the value, carefully unwrap the textfield value, create a new height type identifier, height quantity and height sample and save it to the health store.  Likewise for the weight:

func saveWeightIntoHealthStore () -> () {

var formatter: NSNumberFormatter = self.numberFormatter()

var weight: NSNumber?

if let somethingIsInsideOf = self.weightValueTextField!.text {

weight = formatter.numberFromString(self.weightValueTextField!.text)!

NSLog("The weight entered is not numeric. In your app, try to handle this gracefully.");

abort()

}

if weight == weight {

// Save the user's weight into HealthKit.

var weightType: HKQuantityType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass)

var weightQuantity: HKQuantity = HKQuantity(unit: HKUnit.inchUnit(), doubleValue: weight!.doubleValue)

var weightSample: HKQuantitySample  = HKQuantitySample(type:weightType, quantity:weightQuantity, startDate:NSDate(), endDate:NSDate())

self.healthStore?.saveObject(weightSample, withCompletion: { (success, error) in

if (!success) {

NSLog("An error occured saving the weight sample %@. In your app, try to handle this gracefully. The error was: %@.", weightSample, error)

abort()

}

})

}

}

And we need the number formatter method:

func numberFormatter ()->(NSNumberFormatter) {

var numberFormatter: NSNumberFormatter?

dispatch_once(&onceToken, {

numberFormatter = NSNumberFormatter()

})

return numberFormatter!

}

Since we use this onceToken, we need to define it globally at the top of the class by adding this line below the following imports:

import UIKit
import HealthKit
var onceToken: dispatch_once_t = 0

See you in Part 3!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s