In the last tutorial here I showed you how to create a line graph with PaintCode. In this tutorial we’ll use that line graph to plot some data. To do this we’ll get body mass (weight) data from HealthKit and plot it on the graph. By the end of this tutorial, you’ll see that although it was great to fetch the data from HealthKit and plot it on a graph, the app isn’t really that useful. In part 3, we will add more features that allow you to write data to HealthKit.
You can begin by downloading the basic tutorial where we left off last time found here.
Allowing the Points and Colours to Be Changed
As the current tutorial stands you can only display a static graph which is really of no use. Lets begin adding in the modifications that allow us to adjust the points and colours as needed.
To allow changes to be made we need to make a few modifications to the LineView class. We need to create some public variables. Open up LineView.swift and add the following just below the class definition:
//// Set the UIColors here. Defaults are .red and .gray public var lineColour = UIColor .red public var pointColour = UIColor .red public var horizontalLineColour = UIColor .gray //// The value of the top line on the graph (defaulted to 10) public var height : CGFloat = 10.0 //// point0 is on the far left of the graph. public var point0 : CGFloat = 8.0 public var point1 : CGFloat = 1.0 public var point2 : CGFloat = 2.0 public var point3 : CGFloat = 3.0 public var point4 : CGFloat = 4.0 public var point5 : CGFloat = 5.0 public var point6 : CGFloat = 6.0 //// Linewidth is defaulted to 2.0 public var lineWidth : CGFloat = 2.0
I separated the properties out in between comments to help show some clarity. The first section contains the properties for the colours. It accepts a UIColor and each has a default value meaning that if you don’t set the property the line and points will be red and the horizontal graph lines will be grey.
The second section contains “height” which is what we will set the top value on the chart. If you go up to 95, you might want to round up to 100 for example. I manually specified a CGFloat here so that we don’t have to cast in the drawRect method.
The third section contains CGFloat properties for each point each being the Y value for each point. Each is defaulted to a number although you might want to set them all to 0.0 for practical purposes. An alternative way to do this would have been to create a single array property requiring 7 CGFloat values.
The last property sets the lineWidth to 2.0 as default.
All of these could be done in different ways. You could create a public method that sets all of them privately instead, but keeping them exposed publicly has benefits too such as being able to access the value at a given point.
Another change needed to get this working is to modify the drawRect function.
Graphs .draw_7PointLine(frame: self.frame, resizing: .aspectFit, line: lineColour, point: pointColour, horizontal: horizontalLineColour, height: height, point0Value: point0, point1Value: point1, point2Value: point2, point3Value: point3, point4Value: point4, point5Value: point5, point6Value: point6, lineWidth: lineWidth)
Change what is in there already to the what you see above. Rather than specifying numbers and colours, instead we can specify the property names.
When you open the app now, you will see the following:
Manipulating the Graph
The next step is to test that our changes actually can work. Move back to the ViewController.swift file. Note that we added the UIView (which was changed to a LineView in the storboard) as an IBOutlet. This provides the reference we need to start making changes.
In viewDidLoad add the following 3 lines of code:
lineView.point0 = 0.0 lineView.lineColour = UIColor .green lineView .setNeedsDisplay()
On line 1 we set point0 to 0.0 (remember it defaults to 8.0 when we don’t touch it).
Line 2 we set the line colour to green.
On the last line we call a method from the UIView class called setNeedsDisplay(). This notifies the system that the contents of the lineView (in this example) need to be redrawn.
Go ahead and run the app to see what happens. Although the graph colours are not the nicest, it demonstrates how easy it is to make changes. You are free to edit the other points, other colours etc… to play around and see what happens. One thing to note is that there are more ways than just setting a UIColor as .green for example. You can use the init methods here to specify RGB and alpha values as follows:
lineView.point0 = 0.0 lineView.lineColour = UIColor .init(red: 46/255, green: 90/255, blue: 201/255, alpha: 1.0) lineView.pointColour = UIColor .init(red: 46/255, green: 90/255, blue: 201/255, alpha: 1.0) lineView .setNeedsDisplay()
To get the values you need something that provides you with the RGB values. In my case I opened PhotoShop, selected a colour and then looked at the RGB values. You need to convert these to a decimal representing percentage, so a 0.0 is zero percent meaning that the specific channel has no intensity and a 1.0 means a channel has full intensity. The RGB values goes from 1 – 255 meaning that we need to divide each value in to 255 to get its intensity. 46/255 for the R channel above means that red has an 18% intensity or 0.18 in decimal.
Moving on to HealthKit
Now that we have a graph that we can manipulate as needed, lets start looking at HealthKit and how to pull in the last 7 days of weight values.
Just a few notes to start with. If you plan on creating an app that requests from the user permission to use their data stored in HealthKit, you would be wise reading through the documentation that provides the restrictions such as not being able to use the data for advertising purposes or disclosing the data without permission. The rules for HealthKit are found in the API reference for HealthKit which can be found here. Also note that a privacy policy is needed should you publish an app. For your own testing, we can assume that you are OK viewing your own health data that you have recorded. Also, spend time studying the technical details for HealthKit as there are some good practices to use such as how your app enters the background and what to do with the data. None of it is particularly complicated if you implement things one step at a time, although it might be easy to be overwhelmed if you try rush through the documentation.
Enable HealthKit
The first step is to enable HealthKit in the capabilities section. This is done by selecting the project which is at the top left of the project navigator (blue icon) and then making sure the “Weight” target is selected. Click on Capabilities and then scroll down to HealthKit. Switch it on. After a few moments 4 steps will be checked off and you are good to go.
Add the following to ViewController.swift:
import HealthKit
In viewDidLoad, add the following (note that we do not need the customisations we used earlier for the graph. Feel free to delete those few lines of code).
if HKHealthStore.isHealthDataAvailable() { // add code to use HealthKit here... print("Yes, HealthKit is Available") } else { print("There is a problem accessing HealthKit") }
When you run the app now, you’ll either be told in the console that we are good to go or not. When you want to ship an app you need to decide how to handle a situation where HealthKit is not available, such as on an older device running an older version of iOS.
Create a new class called HealthManager from the “Swift File” source. When created, declare the class as follows (and add the import statement at the top):
import HealthKit class HealthManager { }
Add a public constant called healthStore:
public let healthStore = HKHealthStore()
Create the following (before the last closing curly brace):
public func requestPermissions() { }
So far we have created a class, added the healthStore as a public constant so that the calling class can access it. We have also declared a method called requestPermissions().
In this method add the following:
public func requestPermissions() { let readDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)!] healthStore.requestAuthorization(toShare: nil, read: readDataTypes, completion: { (success, error) in if success { print("Authorization complete") } else { print("Authorization error: \(String(describing: error?.localizedDescription))") } }) }
Lets step through this one line at a time.
Line 2. This is where we request what kind of data we want to read. In this instance, we only want access to the bodyMass which according to documentation is referring to the users weight. If you wanted to access 2 or more types of information I suggest you break this line down in to separate lines such as declaring a HKObjectType and storing that in a constant, and then doing the same. You may have noticed that we also put this in to a Set. Specifying several in a long format might make it harder to read.
On line 4 we begin to request authorisation. Here is what happens:
We call .requestAuthorization. Because we are not writing data, we declare the first parameter as nil. The second is what is read which we call the readDataTypes Set in from line 2. We then have a closure which has 2 parameters; success is a Bool and error is an Error. We will declare this inline (using the word “in”). If success is true, this process worked although a success does not mean that the user granted permission. It just means that no errors were found, so even if successful, the user may have declined giving permission. We will check for this later.
Move back to ViewController.swift and add the following to viewDidLoad in to the “isHealthDataAvailable” if conditional statement:
print("Yes, HealthKit is Available") let healthManager = HealthManager() healthManager.requestPermissions()
Line 1 already exists from earlier, but was just added in to inform you of where to put the next lines.
Line 2 we declare a constant and initialise that with HealthManager() (our custom class).
Line 3 calls .requestPermissions() which is the method we just wrote in our HealthManager class.
Just before running the app, check Info.plist and add the following key if needed:
NSHealthShareUsageDescription.
This key contains the description that you will show to the user on the Health Access view that we will work on displaying in a few moments. This is a fairly generic message that you will write with just a brief description why you might want to access the users health data (see the message under “App Explanation:”).
Go ahead and run the app. You should be prompted to allow access. If you allow “WEIGHT” to access “Weight” (confusing huh!?… this is because the app is called Weight and wants to access Weight, as in Body Mass) then the “Allow” button top left is enabled. The Health Access view will be dismissed and you’ll see a message in the console saying “Authorisation Complete”. Remember that this message would also show (because we told it to) even if the user denied access. We only know at this point that the Health Access view was displayed and that the user did something.
If the request was successful, we can use the closure to initiate the request to fetch the necessary data so that we can update the graph.
Fetching Data from HealthKit
With many services we access on the phone it is often wise to check the authorisation status such as when you want to access the users location so that the app doesn’t crash when you attempt to get a location but cannot. However, when reading from HealthKit you do not need to check the status. Apple doesn’t make that information available. Instead, it either provides no information if the user denied the request, or it provides information written by the app and nothing else.
“To help maintain the privacy of sensitive health data, HealthKit does not tell you when the user denies your app permission to query data. Instead, it simply appears as if HealthKit does not have any data matching your query. Your app will receive only the data that it has written to HealthKit. Data from other sources remains hidden from your app.” Link
So lets get started on retrieving data. On the initial load of the app we can use the closure to kick off the request. In the request permissions method in HealthManager.swift add the following to the if success section:
self.fetchWeightData()
Note that “self” is needed here before calling the method. This is due to working with a closure, but I won’t go in to details here as it warrants a separate post.
Your requestPermissions() method will now be as follows:
public func requestPermissions() { let readDataTypes : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)!] healthStore.requestAuthorization(toShare: nil, read: readDataTypes, completion: { (success, error) in if success { print("Authorization complete") self.fetchWeightData() } else { print("Authorization error: \(error?.localizedDescription)") } }) }
You’ll have an error now. To fix that, add the following as a new method:
func fetchWeightData() { print("Fetching weight data") }
This is where we’ll start to read all the data from HealthKit. To accomplish this we need to set up a few things. These are:
1. Specify the Quantity Type (what information we are querying).
2. A start date.
3. An end date.
4. A predicate.
Quantity Type
Earlier, we sought permission to fetch bodyMass (aka, the users weight). To run a query on HealthKit we need to specify what information we are getting. We may have opted to request access to several types of information from HealthKit. When we run the query, we are letting it know in this instance, which data we want.
To do this, we need to create a Set as follows:
let quantityType : Set = [HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.bodyMass)!]
We are specifying the quantity type here (in this case, bodyMass from the HKQuantityTypeIdentifier).
Dates
We need to create a predicate that contains dates so that we can search for data between 2 dates. We create the dates as follows:
let startDate = Date.init(timeIntervalSinceNow: -7*24*60*60) let endDate = Date()
startDate is taken from “now” and counts back in seconds. In this example we are fetching 1 week of data which is 7 days, 24 hours, 60 minutes, 60 seconds. We then – that to go backwards from todays date.
endDate is is just the date now.
The Predicate
We need to create a predicate so that the query can use it to determine what information to get. The predicate is composed of the quantityType, startDate, and endDate.
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
Now that the predicate is created, we can use it in the query to fetch the data. We use HKSampleQuery for this and initialise it with the sampleType, predicate, a limit, and sortDescriptor. We also have a results handler which is called when the data has been fetched. Here is the full query:
let sampleQuery = HKSampleQuery.init(sampleType: quantityType.first!, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil, resultsHandler: { (query, results, error) in DispatchQueue.main.async(execute: { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "BodyMassDataAvailable"), object: results as! [HKQuantitySample], userInfo: nil) }) }) self.healthStore .execute(sampleQuery)
Lines 1 to 5 is where we call the init method. Note that we store the result in sampleQuery as we need this on line 13 to execute it.
sampleType is the first object of quantityType from earlier. Because we only had the 1 quantityType stored in the set, we can call the first object. This isn’t the ideal way, but is acceptable for this example.
We pass in the predicate on line 2.
Line 3 we set that there is no limit on the amount of data we are collecting. We could widen the dates but still restrict it to 7 results here if needed.
Line 4 tells it that we do not want the data sorting.
The results handler on the remaining lines are called when the query is executed (line 13) and completes.
You will notice on lines 7, 8, and 9 that I post a notification with the result and pass that as a HKQuantitySample as the object. Essentially, we have an observer in the mainView listening out for the notification with name BodyMassDataAvailable. When the query completes, it posts the result as a notification for any listeners to pick up on.
This completes the HealthManager.swift file for now. Please note that no error checking is included here. If you are to use this code (which you are free to do so), please first check for errors in the results handler, and also make sure that you handle a situation if no results are returned.
Using the Results to Create the Graph
Moving back to the mainView, we need to add an observer. This can be added in viewDidLoad just before the check we do to see if health data is available.
NotificationCenter.default.addObserver(self, selector: #selector(updateGraph(notification:)), name: NSNotification.Name(rawValue: "BodyMassDataAvailable"), object: nil)
We add an observer here that listens out for BodyMassDataAvailable. When triggered, it calls the updateGraph(notification:) selector. You will get an error at this point because that method hasn’t been created. Lets add it now:
func updateGraph(notification: Notification) { }
So going back a few steps, the query is run in the HealthManager class, the results are collected, the resultsHandler is called, and a notification is fired along with the results which are passed along with it.
At this point we can write print(notification.object) and see the results in the console.
Displaying Actual Results on the Graph
We now have the results in the mainView; it’s time to plot them on the graph.
We first take the notification object and store it in weightArray:
let weightArray = notification.object as! Array
We specify AS an array containing HKQuantitySample. This is because we need to inspect the objects in the next few steps to determine the highest value. Specifying the type helps us do this.
In the next step we want to find the highest sample out of the 7 that were returned. This will be used to determine what the highest value is (rounded up to the nearest hundred). We do this as follows:
let maxSample = weightArray.max { a, b in a.quantity.doubleValue(for: HKUnit.init(from: .pound)) < b.quantity.doubleValue(for: HKUnit.init(from: .pound)) } We use the .max function of the Array class here to determine which of the HKQuanitySample's contains the highest quantity value. Notice how we need to drill in to each HKQuantitySample to get to the data we need. First, we access the quantity property, but this contains a HKUnit object. There's a property of HKUnit that provides the doubleValue, but we need to specify what we want the doubleValue format to be in. In this instance, I specified the result to be in pounds. This doesn't really matter because we are comparing matching quantity types to each other and with weight, it we compare pounds to pounds or kilos to kilos, the correct answer will be obtained. The end result is a double value stored in maxSample. The next step is to round up that number:lineView.height = CGFloat.init(100 * ceil((maxSample?.quantity.doubleValue(for: HKUnit.init(from: .pound)))!/100))We also have a lengthy line of code here. Our PaintCode chart requires a CGFloat, so the first step is to initialise a CGFloat with a double value. We get the double value in a similar way to how we got the max value earlier by getting the doubleValue property of quantity, and then specifying we want the result in pounds. We use the ceil function to round up, but multiple first by 100, and then divide the maxSample by 100. This would round 130 up to 200, and 90 up to 100.
The result is then used to specify the maxHeight of the chart.
We then need to define each point on the chart. X is already fixed, so we just need to specify the where on the Y axis each result should be.
NOTE: This app in it's current state assumes that your own HealthKit has weight stored for the previous 7 days. If any days are missing, it will crash... so go ahead and guess a few days if needed. You can delete them afterwards.
lineView.point0 = CGFloat.init(weightArray[0].quantity.doubleValue(for: HKUnit.init(from: .pound))) lineView.point1 = CGFloat.init(weightArray[1].quantity.doubleValue(for: HKUnit.init(from: .pound))) lineView.point2 = CGFloat.init(weightArray[2].quantity.doubleValue(for: HKUnit.init(from: .pound))) lineView.point3 = CGFloat.init(weightArray[3].quantity.doubleValue(for: HKUnit.init(from: .pound))) lineView.point4 = CGFloat.init(weightArray[4].quantity.doubleValue(for: HKUnit.init(from: .pound))) lineView.point5 = CGFloat.init(weightArray[5].quantity.doubleValue(for: HKUnit.init(from: .pound))) lineView.point6 = CGFloat.init(weightArray[6].quantity.doubleValue(for: HKUnit.init(from: .pound))) lineView.setNeedsDisplay()The next lines see us access each item in the array. We would not normally use this way to manually assume that data is contained, but for a quick test with known data, this is OK.
Each line accesses the quantity as a double value in pounds (like we have seen previously).
The last line calls setNeedsDisplay() which redraws the chart to put the points in the correct place.
You can go ahead and run the app now. You should see your weight data plotted on the graph.
Making it Useful
As you have seen, the app is not flexible at all. The top line is too high, and the bottom line is too low to really show any useful changes in the weight data. It is also unlikely that someone will have weight data stored every single day for the past week. I'm not saying that won't happen, but for many it won't be the case. There are a number of changes that can be made to make it more useful. We could create other graphs in PaintCode to show week, month, year, and then have the app plot the points it has and ignore the ones it doesn't. We could also bring the range a little closer rounding it up and down to the nearest 10 or 20 instead of dropping all the way down to zero. This would make variations more noticeable on the graph.
Experiment with the code and see if you can create something useful from it. I suggest looking at what other queries can be used. One query can let you know what units are used for storing bodyMass in HealthKit. You can fetch that information and then change the .pounds to the result of that query so your graph matches the preferred units you set in HealthKit.
You can download the tutorial here. Please post any questions in the comments below.
Leave a Reply
You must be logged in to post a comment.