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.
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
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
In your own code, you need at least two delegate callbacks:
homeManagerDidUpdateHomes(_:), to learn when the database has loaded or
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
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:
- It captures a snapshot when the party starts, saving each bulb’s current
- It sets all bulb colors on a timer during the party
- It restores the snapshot when the party ends (
These methods are all marked
async, and they call the async versions of the
HomeKit methods using
await. They also use
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:
- At the root lies your HomeKit database, represented by
- A HomeKit database contains homes. You may have more than one home, with one of them designated as your primary home.
- A home contains accessories; these are the devices managed by or known to HomeKit. Each accessory advertises a type. Each of my Nanoleaf bulbs is an accessory of type “lightbulb”.
- Each accessory contains one or more services; these are the logical, manageable entities which your app talks to, and they each also have a type. Each bulb accessory contains a lightbulb service. This represents the ability to turn on and off, to control brightness or hue, and so on, and is distinguished from other services (for example, a software-upgrade service) on the same device.
- Each service exposes a set of characteristics, which are the atomic attributes of the service, such as bulb brightness or product software version. Each one has a type, a value, and indicators as to whether it may be read and/or written. This reading and writing is the asynchronous work noted above, and is how you control your network.
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.
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.