Streaming is available in most browsers,
and in the WWDC app.
Synchronize health data with HealthKit
HealthKit provides you the tools to smartly manage health data anywhere, whether across multiple HealthKit-enabled devices such as iPhone and Apple Watch or with an external server to share data across a care team. In this session, we'll dive into managing data versions via HealthKit's built-in sync identifier metadata, how to detect changes in health data using an HKAnchoredObjectQuery, and cover best practices for ensuring you're always working with the right data everywhere.
Hello and welcome to WWDC.
Hello, everyone, and welcome to "Synchronize Health Data with HealthKit". My name is Netra, and I'm a software developer on the HealthKit team. Our users store their most private and personal information on our devices in the form of health data. On Apple platforms, HealthKit is the foundation that provides easy access to this data.
HealthKit also enables all of the rich experiences you have created in your apps. As a result, we are all part of this large health ecosystem. The goal of this health ecosystem is to empower our users. Our users may access their health data from any part of this ecosystem. From Apple Watch, from the apps released by Apple all from all your amazing apps.
As such, we never want to surprise our users with the changes we make to their health data. While we all want to be good citizens of this health ecosystem, it can get quite challenging. In this talk, we here at HealthKit are going to help you do just that. So let's remember that our goal is to empower our users. Users must always remain in control of their data and changes to health data must always reflect user intent.
Today, we look at two important topics. First, we look at monitoring changes in HealthKit.
We know that data can be read or written from any part of the health ecosystem. This health data can be used to power multiple types of apps or data visualizations. Our apps must be equipped to react appropriately to the changes in HealthKit.
We will then dive into maintaining an external data store in sync with HealthKit, and we'll see how HealthKit can make this really easy for you.
We'll begin with our first topic, monitoring changes in HealthKit. Let's build an app to work through these concepts. This is an app for patients recovering from an injury. This app let's patients track their daily step count. As time goes on, they want to monitor the progress in their daily step count. Additionally, our patient also visits the physical therapist a couple of times a week and completes certain walking tests. The test is a six-minute walking distance test. It measures the distance covered by a patient while walking on a flat surface for six minutes. The physical therapist enters the results of this test on their machine and data is synced over to the user's iPhone as a weekly report.
One of the best ways to monitor such important data is via a graph. A patient can easily see the uptrend or downtrend in their steps count with out being too concerned with actual values. Here we have a graph that is of great interest to our patient and the physical therapist. It tracks the patient's total step count over each day of the past week. Now steps can be recorded from both Apple Watch and iPhone. We want to ensure that we get an accurate count of steps taken in a day. The first thing you do when creating a graph like this is to reach for the HKStatisticsCollectionQuery. This is the first query you should reach for when creating most graphs. You might have already learned all about this query in our previous talk, "Getting Started with HealthKit". If not, and if it is new to you, you might want to go watch that talk first. In fact, we'll use the SmoothWalker app created in that talk to build our app.
The graphs that we just created need to be sent to the remote server so that the physical therapist who is interested in the user's progress can read them as well. How would this look like? First, we'd send our initial graph of the week to the server.
Next, if there are any changes to the data for the week, for instance, with more steps collected, the new data for the week would have to be sent over to the server. For our purposes here, we are considering the external data to be a remote server, but it might as well be an external database maintained locally on the device. It could be using Core Data or even a SQLite database.
Now we know that we can use the HKStatisticsCollectionQuery to load data, we'd need to run the query at a regular cadence to fetch new data. This could be at app launch and maybe every few hours after that.
But there are some downsides to using it for reacting to new changes.
If data does not change often enough, we would be running the HKStatisticsCollectionQuery multiple times, leading to redundant calculations.
We would also be sending all the data every single time. This would be a waste of network resources.
HealthKit has another tool specifically for use cases like this. It's called the HKAnchoredObjectQuery. The HKAnchoredObjectQuery allows us to monitor updates to the health database.
It provides a snapshot of changes in the health database. This snapshot includes both new samples and deleted samples. Let's see how this query works.
As the name suggests, an anchored object query requires an anchor.
An anchor represents a specific point in time in the evolution of the health database. Health data could have been added or deleted after this point in time.
This anchor allows you to identify all the samples you last received from this query.
When you provide HealthKit with this anchor, HealthKit will only return the changes since that point.
Now, initially, for the first query our anchor will be nil.
HealthKit at this time has samples A, B and C.
When you execute the query, HealthKit returns all the data based on the data type you specified. In this case, you will receive samples A, B and C in your updateHandler.
The anchor is updated, and HealthKit gives you the new anchor in the updateHandler.
It is the last point in time that HealthKit returns samples in the updateHandler.
Let's say there were more samples added since your updateHandler was last called. These include samples D and E. Additionally, sample B has since been deleted. For every subsequent query run, only the changes since the previous anchor will be returned in the updateHandler. These will include samples D and E, the deletion of sample B, but nothing about A and C.
When we use the HKAnchoredObjectQuery or any query in HealthKit, we must think a little about the type of data and use case we are dealing with. In the case of steps, we don't really care about the actual samples itself. In fact, Apple Watch was released five years ago now. That's a lot of data being generated by these devices. That's a lot of data to sync up. We just want a cumulative statistic, not every single sample.
There are other cases where we might care about the individual samples. These could be samples that are not generated very often. Each data type can be treated differently. We must spend some time thinking about how we want to query and sync our data. The task we are trying to accomplish drives the type of query we want.
Querying for the minimal amount of data has its performance benefits as well. For our current use case, we just want the statistical graph to be available to the physical therapist. How do we do this? We can always run over our samples from the HKAnchoredObjectQuery and compute the graph. But why not make the two queries work together? With the anchored object query, we can set our predicate and sample type to exactly what we are looking for. When the anchored object query updates you with changes in HealthKit, you can look at the dates of the returned samples and use that to create and run the statistics collection query.
The statistics returned for those days can then be sent to the remote computer as updated graph data.
Now we just send the new data to the server.
This, again, might be a local Core Data model or an NSURLSession for a remote server. To understand this better, we'll take a look at how this would look like in code.
First, we'll set the parameters for the anchored object query. The sample type is step count.
We'll set the anchor parameter using the PersistedAnchor. Persisting the anchor allows us to only retrieve the changes in HealthKit since the last query.
We need the same behavior for the initialResultsHandler and the updateHandler, and so we'll set the same block for the handler variable.
In the handler, we'll unwrap the samples returned from HealthKit.
We'll create a predicate from the samples. This might be the dates for which the samples were retrieved. This is the predicate we'll be using to initialize our statistics collection query.
Here we also need to update the PersistedAnchor.
At the end of this block, we can call the fetchStatistics method that creates the HKStatisticsCollectionQuery using the predicate.
We'll initialize the HKAnchoredObjectQuery with the parameters we set. These include sample type, a nil predicate, the anchor and lastly, the resultsHandler and the updateHandler.
Finally, we'll execute the query on the healthStore.
Let's see how the fetchStatistics method creates the HKStatisticsCollectionQuery. Once again, we set our parameters. We want our statistics collection query bucketed by days. To achieve this, we'll set the anchor date to Monday at midnight and an interval parameter of one day. The sample type will once again be step count.
We can then initialize the HKStatisticsCollectionQuery with these parameters. The options will be cumulativeSum since we want a total step count for each day.
As you can see, we have provided the predicate created in the anchored object query over here.
In our initialResultsHandler, we unwrap the statisticsCollection and then simply send over this data to our remote server.
And as always, we only get our updates after we execute the query on the healthStore.
We just saw how you can send data from HealthKit to an external server. Now we need to get to the next step, which is receiving data from the external store and saving it to HealthKit as well.
Like we saw earlier, our patient visits the physical therapist's office a couple of times a week and completes six-minute walk tests.
The six-minute walk test is the amount of distance covered by an individual while walking for six minutes on a flat surface. It is often used by physical therapists to gauge their patient's exercise capacity.
This year, we introduced a new set of mobility types in HealthKit. One of the data types is the six-minute walk test distance. These work perfectly for the purposes of our app. We can save test data received from the physical therapist as six-minute walk test distance samples. In our app, the physical therapist records these six-minute walk test values and a weekly report is synced to the patient's device. The weekly report contains a graph of the six-minute walk test distance for each day of the week.
We also have the individual samples from the graph displayed underneath it. This is in case the physical therapist or patient wants to dig deeper into each individual test result. This takes us back to the idea that each data type needs to be treated differently. We must think about the type of data we are dealing with and what our use case is. The sixMinuteWalkTestDistance is not written to HealthKit very frequently, and the patient or the physical therapist might be interested in each individual test sample. So, unlike steps, which we saw earlier, in this graph it is worth plotting each sample. Earlier, we were syncing step samples from the device to a remote server. Now we have this weekly report on the physical therapist's server. We want to sync it from the server to the patient's device and save the corresponding samples in HealthKit.
This will allow the patient to view their sixMinuteWalkTestDistance graph on their app along with the individual test samples.
When saving changes to HealthKit, there are a couple things to keep in mind. We only want to save incremental samples in HealthKit. Simply removing and then re-saving all the data can result in an inconsistent state of the user's health data. New samples reflect new health data about the user, for instance, a new six-minute walk test or new steps covered. Maybe even the change in weight recorded by the user.
When deleting a sample, you need to ensure that it is actually a sample that was previously written by your app. You can't delete data you didn't explicitly save yourself.
A good best practice here is to first query for the sample and then delete it.
Adding or deleting a sample should always reflect user intent. If the user didn't mean to delete a sample, then you should probably not be deleting it.
Now there are some challenges that arise here. What if the physical therapist wanted to update a specific test? For example, for a test on the 18th of June, the patient covered 400 meters in a six-minute walking test. This data was later updated by the physical therapist to reflect an error.
The distance covered was actually 450 meters. Now this can be a little tricky. When you edit a sample, you need to actually delete and add a new sample. If you don't, you could be saving duplicated samples. This means you have to query for the sample, match it to the exact sample the physical therapist had edited and then save a new sample.
If there are no edits needed to be made to the other samples, you need to make sure that during your changes you are not saving a duplicated sample for any of those either.
Health data is available on all of the patient's devices: their iPhones and their Apple Watches. The change in samples need to be reflected correctly across all devices. If you have saved a sample on one device, you need to make sure that you are not saving the sample again on another device.
All this time, you have to ensure that you're correctly reflecting user intent. This may appear to be really complex, but HealthKit actually makes this really easy for you.
HealthKit contains two metadata keys known as the HKMetadataSyncIdentifier and the HKMetadataSyncVersion. The sync identifier is a string, and the version is a number.
The identifier allows us to recognize a sample anywhere in the health ecosystem across any of the user's devices. The version helps us understand when the sample has been updated.
When you set a sync identifier on a sample, HealthKit ensures that duplicate copies of the sample are not saved in the user's health database.
A combination of the sync identifier and the version allows HealthKit to update samples only when the version number has increased.
Additionally, all operations done using sync identifiers are transaction safe. That means if there was any error, you can be rest assured that your data is in a consistent state. Health data is available on all of the user's devices. Sync identifiers allow you to maintain samples in a consistent state across devices.
In your remote server, you have a sample with an identifier and a version one.
Your app syncs a sample from the remote server to the patient's iPhone.
Considering that this is the first sample, HealthKit saves it successfully.
Now when HealthKit realizes a new sample has been saved, it syncs this over to all of the patient's devices. If the patient had an Apple Watch, it would sync it over to the Watch.
If your remote server were also syncing data to your app on Watch and you try to save the sample again, HealthKit would see that the sample already exists and ignore it.
Now if the physical therapist decides to update the distance completed in the six-minute walk test, we would update the sample by keeping the sync identifier consistent but increasing the version number.
When this sample is synced to the patient's device, HealthKit notices that the version number has increased.
It overwrites the previous sample with the new sample.
The sample will again be synced to all of the patient's devices.
If the remote server syncs the version two of the sample to Apple Watch, HealthKit would see that the sample already exists and ignore the sample.
As you can see, HealthKit will manage all the conflict resolution while saving and syncing. The challenging task of versioning and syncing has been reduced to simply maintaining consistent identifiers. We saw the patient's weekly report earlier, but how do we go about actually modeling this data? One way to do this is by representing the weekly report as a report class. This report class can be identified with a high-level sync identifier.
The report class will contain a list of all the sixMinuteWalkTestDistance samples from that week.
Each sample can then contain a sync identifier metadata key which is derived from the high-level report identifier.
This way, each sample can be uniquely referenced across different weeks.
Data can be synced from the remote server to the patient's device in the form of this report class model, and individual HK samples from this list can be saved to HealthKit.
Let's take a look at this in a demo. We have already created this app, SmoothWalker, in our previous talk "Getting Started with HealthKit." This project is also available for download as sample code on the Developer website. We want to create this weekly report view controller in our app. When you press the Fetch button, we want to pull the six-minute walk samples from the server and populate our view controller with it. Let's take a look at the WeeklyReportTableViewController class. When we select the Fetch button, the didTapFetchButton method will be called.
Here we want to pull the server response from the network.
We then want to call the handleServerResponse method with the serverResponse. Let's implement the handleServerResponse method.
Here we want to pull the weekly report from the serverResponse and save the corresponding samples to HealthKit.
The first thing we'll do is to pull the weekly report from the serverResponse.
After this, we loop over all the samples in the weekly report. As you can see here, we are looping over the weekly report samples and returning an HKQuantitySample for each of them.
In this loop, first we'll set the parameters for HKQuantitySample.
We have a quantity which has a unit meter, and the value is pulled from the serverHealthSample. The sampleType is sixMinuteWalkTestDistance. And finally, we have the start and end date.
We'll then initialize our HKQuantitySample with these parameters.
We have the sample type, the quantity, the start and end date, and, for now, the metadata is "nil." The samples returned from the loop need to be saved to HealthKit.
So now, we'll save the samples to HealthKit.
As you can see, we have saved all these samples using the healthStore, and the completion handler will load the new data from HealthKit to the view controller. Let's run this code and see what it looks like.
When we select the Fetch button, the view controller is populated with the samples from the weekly report.
However, when we select the Fetch button again, we can see that duplicated samples are saved to HealthKit. This is incorrect. We don't want duplicated data in HealthKit. Let's see how this changes when we include metadata in the HKQuantitySample. We create a metadata dictionary and add it to our list of parameters. We'll pull the sync identifier from the serverHealthSample and add it to the key, the HKMetadataKeySyncIdentifier. Similarly, we'll also pull the syncVersion from the serverHealthSample and add it to the key, the HKMetadataKeySyncVersion. We'll then add this metadata dictionary to our list of parameters in the HKQuantitySample. Let's run this code again and see what it looks like.
For the purposes of this demo, I've added code to delete all the six-minute walk samples in HealthKit on app launch. However, let's remember that when deleting samples, always be careful and ensure that we're reflecting user intent.
Now, on selecting Fetch, the six-minute walk samples are displayed in the weekly report. On selecting Fetch multiple times, however, there are no duplicated samples. As you can see, saving a sample with the HKMetadataSyncIdentifier and the HKMetadataSyncVersion ensures that there are no duplicated samples in HealthKit. We have now modeled the data backing this graph. Synchronizing your external data with HealthKit is not as challenging as it might appear to be. HealthKit provides the tools that allow you to efficiently monitor changes in the health ecosystem, as well as maintain your data consistently across multiple devices. Let me leave you with some best practices when working on synchronizing your health data. When making changes to users' data, ensure that you're always reflecting user intent. Users should not be taken aback or surprised by the changes to their health data.
Think about ways in which you can run efficient queries. Maybe you can consider combining queries to fetch and sync minimal amount of data.
Finally, since health data exists across multiple devices, use sync identifiers and version numbers to keep data consistent across devices.
We have only touched a small part of the health ecosystem on Apple platforms, but there is much more you can do here. When working with health data, think about the security and privacy implications of what you are trying to do. This is especially important if we're maintaining health data on an external data store. Apple has a lot of resources that can help you with this.
If you want to create the beautiful graph visualizations we showed you today, take a look at the CareKit framework. CareKit is an open-source framework for developing apps specifically around managing your health and providing care. Finally, HealthKit has multiple other features that you can use to create rich health experiences. These range from workouts, clinical health records and high-frequency data types. We are so excited to see the amazing apps that you create using all these HealthKit resources. Thank you so much for watching. I hope you have a great WWDC.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.