For todays tutorial we will look at reading characteristic data from HealthKit. This is one of several ways in which we can read data, with the other ways being queries and long-running queries. We will investigate queries and long-running queries at another time.
Direct method calls can be used to the HKHealthStore to read characteristic data. These items stored in HealthKit do not require a query to be made. These are characteristic types of data such as sex, date of birth, and blood type, to name a few. The reason for this is that these types of data do not change over time. For example, your birthday remains constant.
You can access characteristic data directly through the health store. In a previous tutorial we covered how to setup Apple HealthKit. We will continue on from this project, which you can download here.
In that previous tutorial we requested permissions for stepCount and biologicalSex. Out of these two, biologicalSex is the only one which is characteristic data. We will look at how we can request this data from Apple HealthKit.
Lets look at the instance method called biologicalSex(). We see in the declaration that this method throws a HKBiologicalSexObject. In the discussion section we learn that we need to call this method in a try expression. Full details on how to do that can be found on the Swift website in the Error Handling section, but for the purposes of this tutorial, we will just use try! to disable error propagation, although we do cover errors towards the bottom of this tutorial.
if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.female { print("You are female") } else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.male { print("You are male") } else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.other { print("You are not categorised as male or female") }
The code above is fairly self explanatory. We use an if statement to check if the biologicalSex stored in the phone is female, male, or other and the appropriate message will be printed to the screen. Other values include “not set” which is not demonstrated above.
One thing to always make sure is that if you are requesting a characteristic that you have requested authorisation from the user to access this information.
If you implement the code as is above, but have not requested permission from the user, the app will crash with the following error in the console logs:
Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=com.apple.healthkit Code=5 "Authorization is not determined"
If you have requested permission and the user has declined, the app will not crash, but the if conditions above will not be met, and thus, none of the conditions will be true. In other words, nothing will happen.
A reminder of how this works can be found in the documentation which reads:
To help protect the user’s privacy, your app does not know whether the user granted or denied permission to read data from HealthKit. If the user denied permission, attempts to query data from HealthKit return only samples that your app successfully saved to the HealthKit store.
So there are a few conditions which can cause no data to be made available:
1. You did not ask the user for permission. If you use try!, then your app will crash with code 5 mentioned above. You can prevent the crash from happening by handling the error instead of using try!.
2. The user declined giving permission. You do not know if the user declined or gave permission to access their data. If you do not see any data they either have not set biologicalSex, or they denied you access, or both.
The code below is the full code for this tutorial so far:
let healthStore = HKHealthStore() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. if HKHealthStore.isHealthDataAvailable() { let readDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!, HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex)!] let writeDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!] healthStore.requestAuthorization(toShare: writeDataTypes, read: readDataTypes) { (success, error) in if !success { // Handle the error here. } else { self.testCharachteristic() } } } } /// Fetches biologicalSex of the user. func testCharachteristic() { if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.female { print("You are female") } else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.male { print("You are male") } else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.other { print("You are not categorised as male or female") } }
A very brief overview:
Line 17 is called when the request authorisation is successful. Remember that even if they decline all permissions this is still classed as being successful. In other words, the success does not depend on what the user selects. Errors might be thrown when the user is using iOS 7 for example (instead of iOS 8+).
Line 26 checks the biologicalSex property and checks if == female. If so, the user has set their sex to female in Apple Health. Likewise, lines 28 and 30 are similar.
Date of Birth Characteristic
The next example we will look at is how to access the date of birth characteristic of the user. The first step is to modify our readDataTypes Set to include this characteristic. Typically, the permissions sheet only appears once for the user. However, if requestAuthorization recognises more permissions have been requested by the programmer, the sheet will show again with just those new requests visible. This means that the permissions sheet is only visible ONCE for each characteristic or quantity for each user, unless they delete and reinstall the app.
To request dateOfBirth, replace lines 8 and 9 from above with the following:
let readDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!, HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex)!, HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.dateOfBirth)!]
Line 3 was added. This adds the dateOfBirth characteristic to the Set called readDataTypes. If you run the app again now, you will see that the permissions sheet launches again, but if you have previously run the app with just stepCount and biologicalSex, only Date of Birth for read permissions will be visible. Go ahead and allow it.
In the testCharachteristic function, add the following:
try! print(healthStore.dateOfBirthComponents())
This will print out a DateComponants object. If you gave permission and have your date of birth stored in health, you should see your birth date in the console.
Deprecated dateOfBirth()
While reading the documentation you may have noticed that there are 2 functions to read date of birth. dateOfBirth() is an older method that has been deprecated since iOS 10. If you have users using iOS 8.0-10.0 then you should also implement this method if you need to see their date of birth.
To do this we need to look at availability. To get started, in your project navigate to the project name at the top of the left sidebar. Look for “Deployment Info” and set it to iOS 9.0.
This now allows users from iOS 9.0 onwards to install the app. But if an iOS 9 user runs the app, it will crash because dateOfBirthComponents was only made available in iOS 10.0+.
A new warning will also appear when you build the project:
Go ahead and click on the top “Fix” message and it will modify the code to the following (I added in line 5 before pasting the code here):
if #available(iOS 10.0, *) { try! print(healthStore.dateOfBirthComponents()) } else { // Fallback on earlier versions try! print(healthStore.dateOfBirth()) }
Assuming you have your date of birth set in Apple Health, then depending on if you use iOS 9 or iOS 10, you will receive either a Date object for iOS 9 devices, or a DateComponents object for iOS 10.0 and newer. You can then use that response however you need to in your app.
Handling Errors
I mentioned earlier that we would bypass error checking by using try!. You may have noticed that sometimes the app crashes. This can happen for various reasons such as no data being available for the characteristic, or that the user hasn’t been asked for permission to share that data.
When something goes wrong and you are using try!, the app crashes for the user. The better way is to use try the correct way. Sometimes you know that an error won’t be thrown, and in those cases it might be safe to use try!, but in many cases you shouldn’t disable error propagation.
An alternate way is to convert the error in to an optional value by using try?. When using this, the value returned in the expression is set to nil if there is an error. try? is good for when you have multiple ways to fetch information. Perhaps you want to fetch some information from Health. If that returns an error (or in this case nil), then you might skip on and try another way to fetch the data. Please see example on the error handling page for Swift.
For our scenario, perhaps the better way is to use do-catch. The syntax for this is as follows:
do { try expression statements } catch { statements }
With the fallback code for iOS 9, we could use the following:
// Fallback on earlier versions do { try print(healthStore.dateOfBirth()) } catch { print("There was a problem fetching your data") }
If an error throws in the app it will not crash now. Instead, the catch statements will be run. Here you could write statements to put something on screen to tell the user that their date of birth isn’t available, or you might just quietly fail and carry on execution of the app elsewhere.
If you call a method on line 3 (below) that returns an error, you can respond to specific errors as follows:
// Fallback on earlier versions do { let dateOfBirth = try healthStore.dateOfBirth() print(dateOfBirth) } catch let error { print("There was a problem fetching your data: \(error)") }
Error is now created on the catch, and we can print to the console to see that it’s a nilError.
It is best to handle all errors rather than risking the app crash. You might want to try modify the code to see if you can get it to work with iOS 8.0 or 8.1 where another new error is introduced on line 37 below because .other was made available in iOS 8.2 and above.
You can download the full project here, or copy and paste the code below in to your project:
import UIKit import HealthKit class ViewController: UIViewController { let healthStore = HKHealthStore() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. if HKHealthStore.isHealthDataAvailable() { let readDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!, HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.biologicalSex)!, HKObjectType.characteristicType(forIdentifier: HKCharacteristicTypeIdentifier.dateOfBirth)!] let writeDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!] healthStore.requestAuthorization(toShare: writeDataTypes, read: readDataTypes) { (success, error) in if !success { // Handle the error here. } else { self.testCharachteristic() } } } } // Fetches biologicalSex of the user, and date of birth. func testCharachteristic() { // Biological Sex if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.female { print("You are female") } else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.male { print("You are male") } else if try! healthStore.biologicalSex().biologicalSex == HKBiologicalSex.other { print("You are not categorised as male or female") } // Date of Birth if #available(iOS 10.0, *) { try! print(healthStore.dateOfBirthComponents()) } else { // Fallback on earlier versions do { let dateOfBirth = try healthStore.dateOfBirth() print(dateOfBirth) } catch let error { print("There was a problem fetching your data: \(error)") } } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
Leave a Reply
You must be logged in to post a comment.