I recently spent a little time exploring the HomeKit API with a toy app, and it seemed worth sharing for anyone else wanting a quick start. (I mostly wrote it for fun, while waiting for review for another app, so there aren’t any distractions like unit tests or a good UI.)

We got a set of Nanoleaf RGB LED bulbs for Christmas, which we put in our living room. Inspired by the colored lights which often come on at the end of Apple Fitness+ dance workouts, we wondered if we could easily set them cycling through colors. We wanted a Party Lights mode!

Hemi took to Shortcuts. She got it working pretty quickly, despite some frustrations. There’s no “set light color” action which might take a hue from a list; there’s just has one generic “Control Home” action which needs to be duplicated and configured for each bulb color. Sounded to me like a good chance to try out the API.

Find the repository on Gitlab. It’s just one SwiftUI view and one model class. It finds every lightbulb on the network, lets you choose which ones to control, and has a start/stop button. If HomeKit isn’t configured, or if you deny the app access, it reports that, but doesn’t offer the niceties of helping you configure things. In short, it’s not a shippable HomeKit app, but it works.

HomeKit basics

The first two steps are to add the HomeKit entitlement to your app (in Xcode, this is on the Signing & Capabilities tab under your target) and to add the NSHomeKitUsageDescription key to your Info.plist describing why you want it. (Without that description, your app will crash.)

At runtime, you get started by creating an instance of HMHomeManager and registering a delegate. This automatically checks if the app has permission, prompting the user if they haven’t been prompted yet. If so, it then asynchronously loads the HomeKit database, starts watching for updates, and calls your delegate. (In PartyLights, find this in engageHomeKit().)

In your own code, you need at least two delegate callbacks: homeManagerDidUpdateHomes(_:), to learn when the database has loaded or changed, and homeManager(_:didUpdate:), which is called when authorization is granted or denied. In PartyLights, these two callbacks each just call reload(), which updates the model’s state and list of bulbs.

Within your code, you can synchronously access the data cached in HMHomeManager.

Async/Await and HomeKit

Asynchrony comes in when reading or writing specific values from or to your devices, as this can be quite slow. HomeKit has long provided callback-based asynchronous read/write calls, but it now also provides native Swift asynchronous methods.

PartyLights does the following read and writes:

These methods are all marked async, and they call the async versions of the HomeKit methods using await. They also use withTaskGroup to kick off a batch of async child tasks and await its full completion. Their callers, which are synchronous, use Task to create new tasks to call them from.

What this all means is that each task runs until it hits an await, where it starts the slow call and then returns to its task scheduler. The task scheduler will resume the task, after the await, when HomeKit finishes its work; until then, it’s free to do other work. The entire class is marked @MainActor, so it uses the main actor’s task scheduler, which runs tasks on the main thread and is integrated with the main runloop. So even as all this asynchrony is happening, we can still safely access our shared data, and we don’t block the main runloop from advancing our GUI.

The HomeKit data model

At the most basic level, HomeKit organizes everything into a tree:

Astute observers familiar with HomeKit will notice that I skipped zones (like upstairs/downstairs, within a home) and rooms (which lie within zones). This is because you can directly query accessories from homes, without referencing them at all. Rooms are important for user interfaces, as they group devices together, but your app can ignore them if, like PartyLights, it it doesn’t care about its users.

Colors

One final note about colors.

Lightbulbs in HomeKit use a convenient HSV (Hue, Saturation, Value) model for tuning color and brightness. The hue is stored in one characteristic as a value from 0 to 360, and the saturation and value (brightness) are each stored in their own characteristics which range from 0 to 100.

For PartyLights, I just fixed the brightness and saturation at 100% and 75%, respectively, and cycled the hue in 15-degree increments. The nice thing about this is that you’re only writing one characteristic at a time, so you don’t risk seeing the hue and brightness changing in distinct steps.