Health Up Diaries 1: A Better HealthKit API

Health Up Display Icon

This is to be the first of an ongoing series of my experiences working on my app Health Up Display. I’m taking inspiration from other developers in the community who have done and are doing similar blogs about their own apps. Check out Brent Simmon’s old Vesper Sync Diary or the Slopes Diaries by Curtis Herbert for a few examples.

There are a few goals in launching this series of posts.

  1. A way to talk to myself out loud as I reason through these things.
  2. To possibly draw help from others who’ve already solved similar problems.
  3. Provide that same help to others further behind me in the learning curve.
  4. Use public accountability as a fire under my feet to provide the motivation needed to stay focused and finally ship this app.
  5. To give me a steady stream of content while I am still dipping my toes into the blogging waters.

Before we get started a little background is in order. Health Up Display is my health and fitness tracking app I’ve been working on, sadly, for a few years now. I wanted an app that combined the database viewing aspect of Health.app with a design styling more closely resembling the UI in Activity.app on iOS. I also wanted to address what I feel are a shortcomings in both apps. I like to think of this app as the product of those two apps if they had a baby. I haven’t made as much progress as I would’ve liked on the project yet, but I have made progress nonetheless, and have a very usable beta if you’d like to take a look. You can sign up here to try it out.

With that out of the way, we can move on to the topic of today’s diary entry.

A better API to interact with HealthKit

As I mentioned I’ve been working on this project for a while. Some of the code is really old. Like Swift 2.0 old. Additionally I was just learning Swift and much of it is really just Objective-C code written in Swift. This is partly my fault because I hadn’t yet grocked the language and it’s style was still rapidly evolving in those days. It’s also partly due to the fact that the system frameworks hadn’t, still haven’t in many cases, been truly modernized for Swift yet. This is especially true for the HealthKit framework. On the plus side it is a framework that very much uses closures and fits very well into Swift in that regard, but these closures are really the old Objective-C blocks. Most of the HealthKit APIs are in fact queries that take closures to handle their results asynchronously. Unfortunately these closures usually have parameters in most cases for an optional valid query result and an optional error at the same time. Most Swift developers don’t really like that and we don’t feel it is very Swifty. We prefer things like a solid Result type to clearly distinguish between success or failure with a simple switch statement. Read this post if you need to see what that’s about.

My app has a few classes designed to build these queries for me. They also form a centralized place and wrapper for my app to interact with HealthKit. I use these instances to retrieve health stats from the HealthKit database and then store them in my apps own data cache so the UI can be ready with your most recent stats as soon as you launch. You really don’t want to have to wait on the queries to return every time you launch the app. When the queries finally return newer data later, I update my cache and the UI layer of the app. These classes are full of these previously mentioned examples of code that just isn’t Swifty, and are also crying to be updated now that I understand more about Swift Style and even more about how to use and interact with HealthKit. Additionally soon I hope to get started on the Apple Watch component to this app and I’ll need to share a bunch of this code with that target as well. I have another fun pet project app called Mowing Meter that uses the iPhone’s motion co-processor chip to track your steps and distance while mowing the yard. I’m currently converting it to Swift and turning it into a “real” fitness tracker as well. That project could also utilize much of the same code.

The problem domain is this.

  1. This code needs to be sharable.
  2. This code needs updated to modern Swift naming conventions and less like Objective-C written in Swift.
  3. This modern Swift syntax needs to include a better query return Result type.

The solution

To address problem 1, I’m going to build a shared framework and move all of this code there. It will also provide a good opportunity to rewrite it to satisfy problems 2 and 3. I’m not going to write the entire process here, but I do feel a few examples are in order. For this example we are going to be updating 2 methods. Here they are as currently implemented in all their glory, ahem, lack thereof. The first method creates a query to return quantity samples like heart rate, steps or active calories. The second builds a query to return category samples like stand hours or sleep entries. You can usually safely think of category samples as enum representations of a few possible states for the sample if you’re unfamiliar with them.

Current codeGithub
public typealias QuantitySampleQueryResultsHandler = (_ query: HKSampleQuery, _ samples: [HKQuantitySample]) -> Void
public typealias CategorySampleQueryResultsHandler = (_ query: HKSampleQuery, _ samples: [HKCategorySample]) -> Void

func quantitySampleQuery(startDate: Date,
                         endDate: Date?,
                         dateOptions: HKQueryOptions = [.strictStartDate, .strictEndDate],
                         sampleType: HKQuantityType,
                         resultsLimit: Int = Int(HKObjectQueryNoLimit),
                         resultsHandler: @escaping QuantitySampleQueryResultsHandler) -> HKSampleQuery {
    
    // Build sort descriptor to return the samples in descending order
    let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: false)
    
    // Limit the number of samples returned by the query to the number requested
    let limit = resultsLimit
    
    let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: dateOptions)
    
    // Build the query
    let query = HKSampleQuery(sampleType: sampleType, predicate: datePredicate, limit: limit, sortDescriptors: [sortDescriptor]) {
        (sampleQuery, results, error ) -> Void in
        guard let samples = results as? [HKQuantitySample] , error == nil else {
            resultsHandler(sampleQuery, [])
            guard let queryError = error else { return }
            print("\(queryError.localizedDescription)")
            return
        }
        resultsHandler(sampleQuery, samples)
    }
    return query
}

func categorySampleQuery(startDate: Date,
                         endDate: Date,
                         sampleType: HKCategoryType,
                         resultsLimit: Int = Int(HKObjectQueryNoLimit),
                         resultsHandler: @escaping CategorySampleQueryResultsHandler) -> HKSampleQuery {
    
    // Build sort descriptor to return the samples in descending order
    let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: false)
    
    // Limit the number of samples returned by the query to the number requested
    let limit = resultsLimit
    
    let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [.strictStartDate, .strictEndDate])
    
    // Build the query
    let query = HKSampleQuery(sampleType: sampleType, predicate: datePredicate, limit: limit, sortDescriptors: [sortDescriptor]) {
        (sampleQuery, results, error ) -> Void in
        guard let samples = results as? [HKCategorySample] , error == nil else {
            resultsHandler(sampleQuery, [])
            guard let queryError = error else { return }
            print("\(queryError.localizedDescription)")
            return
        }
        resultsHandler(sampleQuery, samples)
    }
    return query
}

Aside from a few minor modifications from the Swift Migration Assistant over a few iterations these methods haven’t really been changed since the day I wrote them. Let’s break them down a little.

They build a query to retrieve their respective sample types based on the parameters passed in. The generated queries include a completion handler closure which accepts the query, returned samples, and an error if appropriate. As mentioned before that’s just not desirable in a modern Swift API. I did try to tackle that here by having these query factory methods take a closure for processing the results within the apps call site after first logging out an error if need be in the original query completion handler. I would then simply pass in the samples or an empty array to the caller’s results handler. This got the job done but still isn’t really ideal. The original caller gets an array, full or empty, regardless of the result from HealthKit. That’s fine for making the result to the caller non optional so it doesn’t have to be checked there or risk looping through non existent samples while processing them. If we are however looking at sharing this framework in multiple projects, some of them might need to handle a possible error within the original caller. An empty sample array keeps the app from crashing, but it won’t help you determine if there just aren’t any new samples or you may need to tell the app user they might need to provide authorization for your app to access that particular type of sample.

Next these method signatures aren’t very Swift looking and don’t really conform to modern naming conventions at all. To be honest they don’t even really look like Objective-C names to me, more like C++ or dare I say JavaScript, the horror! I did create a few sensible default parameters for a few of them, so you could omit them much of the time when calling, so that’s a plus at least. Also I did create typeAliases for the results handler closures to help keep the method signatures more readable but the rest of the parameters just don’t cut it. Modern Swift naming conventions state we should use prepositions, adjectives and adverbs combined with the types of the parameters to make it a clear readable signature at the call site. These are definitely falling short there.

Lastly when you take a look at those two methods you’ll notice they are nearly identical except for the typecast to the type of sample desired. That’s not really too terrible in the grand scheme of things, but if I decided to update, improve, or fix a bug in one of them I’d have to do it in multiple places to keep them in sync. Okay I take the last statement back a little. That is terrible.

Now that we’ve determined some things wrong and a few things right with these methods, let’s take a look at what I’ve come up with now.

The new and improved codeGithub
public enum Result<Value, Error> {
    case success(Value)
    case failure(Error)
}

public enum HealthUpQueryError: Error {
    case noSamplesReturned
    case undefinedError
}

public typealias BaseSampleResult = Result<[HKSample], Error>
public typealias BaseSampleQueryResultsHandler = (HKQuery , BaseSampleResult) -> ()

public typealias QuantitySampleResult = Result<[HKQuantitySample], Error>
public typealias QuantitySampleQueryResultsHandler = (HKQuery , QuantitySampleResult) -> ()

public typealias CategorySampleResult = Result<[HKCategorySample], Error>
public typealias CategorySampleQueryResultsHandler = (HKQuery ,CategorySampleResult) -> ()

First we have defined a Result type enum in it’s most basic form. You can find a few good libraries like this one on the web if you’d like something a little more full featured. This simple one with a few generics meets my needs though.

Next we’ve defined a simple error type that will give us a few more options on top of the errors already returned by HealthKit queries.

Lastly we’ve defined some more concrete Result types for our queries to return. This way the caller of these methods can perform a simple switch to determine the success or failure of the query and respond appropriately. In the successful case they get the array of new samples and in the case of failure an error is passed on to the caller so it can respond appropriately. I’ve also again created type aliases for our external caller results handlers to keep our function signatures tidy.

Let’s move on to the methods themselves.

Github
private func baseSampleQuery(from startDate: Date,
                             to endDate: Date?,
                             with options: HKQueryOptions = [.strictStartDate, .strictEndDate],
                             for sampleType: HKQuantityType,
                             limitingResultsTo limit: Int = Int(HKObjectQueryNoLimit),
                             using resultsHandler: @escaping BaseSampleQueryResultsHandler) -> HKSampleQuery  {
    
    // Build sort descriptor to return the samples in descending order
    let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: false)
    
    let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: options)
    
    // Build the query
    let query = HKSampleQuery(sampleType: sampleType, predicate: datePredicate, limit: limit, sortDescriptors: [sortDescriptor]) {
        (sampleQuery, results, error ) -> Void in
        guard error == nil, let results = results else {
            guard let error = error else { return resultsHandler(sampleQuery, BaseSampleResult.failure(HealthUpQueryError.undefinedError)) }
            return resultsHandler(sampleQuery, BaseSampleResult.failure(error))
        }
        resultsHandler(sampleQuery, .success(results))
    }
    return query
}

I’ve started with a new 3rd method that serves as a base query generator. This new method holds just about as much of the previously duplicated could as I could get into it. The other two sample queries call this one by passing on their parameters and results handlers. This base query also does the checking for errors reported by HealthKit and passes them off to the completion handler if needed.

Github
func quantitySampleQuery(from startDate: Date,
                         to endDate: Date?,
                         with options: HKQueryOptions = [.strictStartDate, .strictEndDate],
                         for sampleType: HKQuantityType,
                         limitingResultsTo limit: Int = Int(HKObjectQueryNoLimit),
                         using resultsHandler: @escaping QuantitySampleQueryResultsHandler) -> HKSampleQuery {
    
    // Build the query
    let query = baseSampleQuery(from: startDate, to: endDate, with: options, for: sampleType, limitingResultsTo: limit) { (query, result) in
        switch result {
        case .success(let samples):
            guard let samples = samples as? [HKQuantitySample], samples.count > 0 else {
                return resultsHandler(query, .failure(HealthUpQueryError.noSamplesReturned))
            }
            resultsHandler(query, .success(samples))
        case .failure(let error):
            resultsHandler(query, .failure(error))
        }
    }
    return query
}

func categorySampleQuery(from startDate: Date,
                         to endDate: Date?,
                         with options: HKQueryOptions = [.strictStartDate, .strictEndDate],
                         for sampleType: HKQuantityType,
                         limitingResultsTo limit: Int = Int(HKObjectQueryNoLimit),
                         using resultsHandler: @escaping CategorySampleQueryResultsHandler) -> HKSampleQuery {
    
    // Build the query
    let query = baseSampleQuery(from: startDate, to: endDate, with: options, for: sampleType, limitingResultsTo: limit) { (query, result) in
        switch result {
        case .success(let samples):
            guard let samples = samples as? [HKCategorySample], samples.count > 0 else {
                return resultsHandler(query, .failure(HealthUpQueryError.noSamplesReturned))
            }
            resultsHandler(query, .success(samples))
        case .failure(let error):
            resultsHandler(query, .failure(error))
        }
    }
    return query
}

Finally we have our new methods for the query factory. These methods build the base query and give it a completion handler that determines the results of the query and passes it on to the original caller. The completion handlers these two methods provide either pass on the reported HealthKit error or make sure the samples can be typecast appropriately and that we didn’t get an empty sample array back. In this case an error indicating there were no samples returned is passed on to the caller. They return either of those two cases using my spiffy new Result types, greatly simplifying things at the original call sight.

At first glance the method signatures to these two new methods may look a little wordier and convoluted than the originals, but that’s only here at the definition. At the call sight they look almost magical to me. Here’s what you get when accepting Xcode’s autocomplete. I did add line breaks for a better image.

Accepting Xcode's autocomplete with added line breaks.

Here it is in it’s simplest form using the default parameters and with the result handler expanded for clarity.

Using Default Parameters

The parameter types combine with the labels to make these methods easy to parse and tell what they do. Inside my results handler I can perform a simple switch to determine the appropriate course of action. I’d like to come up with a better label for the limitingResultsTo parameter, but I’m currently stumped for something simpler that conveys the same meaning. It doesn’t hurt that most of my calls to these methods use the variant leveraging the default parameters and I rarely see it. Overall this feels like modern Swift to me and I’m quite satisfied with it. Now I just need to finish the rest of my framework like this and I’ll have a nice API that’ll be a joy to work with.

Do you see Anything I could do better or have other suggestions? If you have any other questions or comments feel free to send feedback or thoughts to me on Twitter or Micro.blog.

*****
Written on