HomeKit and Matter: Smart Home Accessory Control in iOS
Every smart home enthusiast has experienced the “five apps to control five lights” problem. HomeKit was Apple’s answer: a unified framework that lets any iOS app discover, control, and automate accessories from different manufacturers through a single API. With Matter — the cross-platform smart home standard that Apple, Google, and Amazon jointly developed — HomeKit’s reach now extends to accessories that were never designed exclusively for the Apple ecosystem.
This post covers the HomeKit framework from an iOS developer’s perspective: setting up HMHomeManager, querying and
controlling accessories, building automations and triggers, and integrating Matter-compatible devices. We will not cover
creating HomeKit-compatible firmware or the Home app’s UI — our focus is on what you can do programmatically from your
own app.
This guide assumes you are comfortable with async/await and protocols.
Contents
- The Problem
- HomeKit’s Object Model
- Setting Up HMHomeManager
- Discovering and Controlling Accessories
- Building Automations and Triggers
- Matter Integration
- Advanced Usage
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Picture the set of Andy’s house from Toy Story. The living room has smart lights, the bedroom has a smart thermostat, and the hallway has a motion sensor that Woody uses to know when Andy is coming. You are building a companion app called “Andy’s House Controller” that needs to:
- List every accessory in the house.
- Turn the living room lights on and off.
- Set the bedroom thermostat to 72 degrees when Buzz is in charge.
- Automatically dim the hallway lights when the motion sensor detects no movement for 10 minutes.
Without HomeKit, you would need separate SDKs for each manufacturer, different communication protocols (BLE, Wi-Fi, Zigbee, Thread), and custom logic for every accessory type. With HomeKit, all of this collapses into a single, type-safe API.
import HomeKit
// Without HomeKit: manufacturer-specific chaos
// lightBulbSDK.connect(ipAddress: "192.168.1.42")
// thermostatBLE.write(characteristic: tempUUID, value: 72)
// motionSensorMQTT.subscribe(topic: "hallway/motion")
// With HomeKit: one unified API
let home = homeManager.homes.first!
let livingRoomLight = home.accessories.first { $0.name == "Living Room Light" }
// Control it through standardized services and characteristics.
The trade-off is clear: HomeKit requires accessories to be HomeKit-certified (or Matter-compatible), but in return you get a consistent API, local and remote control, Siri integration, and automation — all without writing protocol-specific code.
HomeKit’s Object Model
Before writing code, you need to understand HomeKit’s hierarchy. It mirrors how a physical home is organized:
HMHomeManager— The root object. Manages all homes the user has configured.HMHome— A single home (e.g., “Andy’s House”). Contains rooms, zones, accessories, and automations.HMRoom— A room within a home (e.g., “Andy’s Bedroom”). Rooms are organizational only; accessories do not need a room to function.HMAccessory— A physical device (e.g., a smart light bulb, a thermostat). Each accessory exposes one or more services.HMService— A functional capability of an accessory (e.g., the “Lightbulb” service on a smart light). An accessory can have multiple services — a combo light/fan has both a lightbulb service and a fan service.HMCharacteristic— A single readable/writable property within a service (e.g., brightness, color temperature, on/off state).
Apple Docs: HomeKit — HomeKit framework
This hierarchy is important: you do not talk to accessories directly. You read and write characteristics on services that belong to accessories in rooms within a home.
Setting Up HMHomeManager
Entitlements and Permissions
HomeKit requires specific setup before you write any code:
- Enable the HomeKit capability in your target’s Signing & Capabilities.
- Add
NSHomeKitUsageDescriptionto yourInfo.plistwith a user-facing explanation. - For Matter support (iOS 16.1+), ensure your app also has the
com.apple.developer.matter.allow-setup-payloadentitlement if you plan to commission new Matter devices.
Initializing the Home Manager
HMHomeManager is your entry point. It loads the
user’s home configuration asynchronously through its delegate:
import HomeKit
import os
final class AndysHouseController: NSObject, ObservableObject {
private let logger = Logger(subsystem: "com.pixar.andyshouse", category: "HomeKit")
private let homeManager = HMHomeManager()
@Published var homes: [HMHome] = []
@Published var primaryHome: HMHome?
@Published var accessories: [HMAccessory] = []
override init() {
super.init()
homeManager.delegate = self
}
}
extension AndysHouseController: HMHomeManagerDelegate {
func homeManagerDidUpdateHomes(_ manager: HMHomeManager) {
homes = manager.homes
primaryHome = manager.primaryHome
if let home = primaryHome {
accessories = home.accessories
logger.info("Loaded \(home.accessories.count) accessories in '\(home.name)'")
}
}
func homeManagerDidUpdatePrimaryHome(_ manager: HMHomeManager) {
primaryHome = manager.primaryHome
logger.info("Primary home changed to: \(manager.primaryHome?.name ?? "None")")
}
}
Warning:
HMHomeManagerdoes not return homes immediately. You must wait for thehomeManagerDidUpdateHomesdelegate callback. AccessinghomeManager.homesbefore this callback fires returns an empty array.
Creating a Home Programmatically
If the user does not have a home configured, your app can create one:
extension AndysHouseController {
func createHome(named name: String) async throws -> HMHome {
return try await withCheckedThrowingContinuation { continuation in
homeManager.addHome(withName: name) { home, error in
if let error {
continuation.resume(throwing: error)
return
}
guard let home else {
continuation.resume(throwing: HomeControllerError.homeCreationFailed)
return
}
continuation.resume(returning: home)
}
}
}
func addRoom(named name: String, to home: HMHome) async throws -> HMRoom {
return try await withCheckedThrowingContinuation { continuation in
home.addRoom(withName: name) { room, error in
if let error {
continuation.resume(throwing: error)
return
}
guard let room else {
continuation.resume(throwing: HomeControllerError.roomCreationFailed)
return
}
continuation.resume(returning: room)
}
}
}
}
enum HomeControllerError: Error, LocalizedError {
case homeCreationFailed
case roomCreationFailed
case accessoryNotFound
case characteristicNotFound
case noHomesAvailable
var errorDescription: String? {
switch self {
case .homeCreationFailed:
return "Failed to create home."
case .roomCreationFailed:
return "Failed to create room."
case .accessoryNotFound:
return "The requested accessory was not found."
case .characteristicNotFound:
return "The requested characteristic was not found."
case .noHomesAvailable:
return "No homes are configured."
}
}
}
Discovering and Controlling Accessories
Adding Accessories
Users add accessories through the system-provided setup flow. Your app triggers this with
HMHome.addAndSetupAccessories:
extension AndysHouseController {
func addAccessory(to home: HMHome) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
home.addAndSetupAccessories { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
}
This presents the system accessory setup sheet. The user scans the accessory’s setup code (QR code or NFC tag), and HomeKit handles pairing, key exchange, and network configuration. You do not need to manage any of this yourself.
Reading Characteristic Values
Once accessories are in the home, you interact with them through their services and characteristics. Here is how to read the current state of a light in Andy’s living room:
extension AndysHouseController {
/// Finds a specific characteristic on an accessory by service and characteristic type.
func findCharacteristic(
on accessory: HMAccessory,
serviceType: String,
characteristicType: String
) -> HMCharacteristic? {
accessory.services
.first { $0.serviceType == serviceType }?
.characteristics
.first { $0.characteristicType == characteristicType }
}
/// Reads whether a lightbulb accessory is currently on.
func isLightOn(_ accessory: HMAccessory) async throws -> Bool {
guard let powerState = findCharacteristic(
on: accessory,
serviceType: HMServiceTypeLightbulb,
characteristicType: HMCharacteristicTypePowerState
) else {
throw HomeControllerError.characteristicNotFound
}
return try await withCheckedThrowingContinuation { continuation in
powerState.readValue { error in
if let error {
continuation.resume(throwing: error)
return
}
let isOn = powerState.value as? Bool ?? false
continuation.resume(returning: isOn)
}
}
}
}
Writing Characteristic Values
Writing follows the same pattern. To turn the living room light on, set the brightness, and change the color temperature:
extension AndysHouseController {
/// Turns a lightbulb on or off.
func setLightPower(_ accessory: HMAccessory, on: Bool) async throws {
guard let powerState = findCharacteristic(
on: accessory,
serviceType: HMServiceTypeLightbulb,
characteristicType: HMCharacteristicTypePowerState
) else {
throw HomeControllerError.characteristicNotFound
}
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
powerState.writeValue(on) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
logger.info("Set \(accessory.name) power to \(on ? "ON" : "OFF")")
}
/// Sets the brightness of a lightbulb (0-100).
func setLightBrightness(
_ accessory: HMAccessory,
brightness: Int
) async throws {
guard let brightnessChar = findCharacteristic(
on: accessory,
serviceType: HMServiceTypeLightbulb,
characteristicType: HMCharacteristicTypeBrightness
) else {
throw HomeControllerError.characteristicNotFound
}
let clampedValue = max(0, min(100, brightness))
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
brightnessChar.writeValue(clampedValue) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
logger.info("Set \(accessory.name) brightness to \(clampedValue)%")
}
}
Observing Real-Time Updates
To get notified when an accessory’s state changes (someone physically flips a switch, or another app changes a value), enable notifications on characteristics:
extension AndysHouseController {
func observeAccessory(_ accessory: HMAccessory) {
accessory.delegate = self
for service in accessory.services {
for characteristic in service.characteristics {
if characteristic.properties.contains(
HMCharacteristicPropertySupportsEventNotification
) {
characteristic.enableNotification(true) { error in
if let error {
self.logger.error(
"Failed to enable notifications: \(error.localizedDescription)"
)
}
}
}
}
}
}
}
extension AndysHouseController: HMAccessoryDelegate {
func accessory(
_ accessory: HMAccessory,
service: HMService,
didUpdateValueFor characteristic: HMCharacteristic
) {
logger.info(
"\(accessory.name) > \(service.name) changed to \(String(describing: characteristic.value))"
)
// Refresh your published properties.
DispatchQueue.main.async {
self.accessories = self.primaryHome?.accessories ?? []
}
}
}
Tip: Not all characteristics support event notifications. Check the
propertiesset forHMCharacteristicPropertySupportsEventNotificationbefore callingenableNotification.
Building Automations and Triggers
HomeKit automations let your app set up rules that run locally on the home hub (Apple TV, HomePod, or iPad) without your app running. This is the real power of HomeKit — the logic executes even when the user’s iPhone is off.
Event-Based Triggers
An event trigger fires when a characteristic changes. Here is an automation that turns off Andy’s bedroom light when the door sensor detects the door closing:
extension AndysHouseController {
func createDoorClosedAutomation(
home: HMHome,
doorSensor: HMAccessory,
bedroomLight: HMAccessory
) async throws {
// Find the door sensor's contact state characteristic.
guard let contactState = findCharacteristic(
on: doorSensor,
serviceType: HMServiceTypeContactSensor,
characteristicType: HMCharacteristicTypeContactState
) else {
throw HomeControllerError.characteristicNotFound
}
// Find the light's power state characteristic.
guard let lightPower = findCharacteristic(
on: bedroomLight,
serviceType: HMServiceTypeLightbulb,
characteristicType: HMCharacteristicTypePowerState
) else {
throw HomeControllerError.characteristicNotFound
}
// Trigger condition: door closes (contact state becomes 0).
let triggerEvent = HMCharacteristicEvent(
characteristic: contactState,
triggerValue: 0 as NSNumber
)
let trigger = HMEventTrigger(
name: "Andy's Door Closed",
events: [triggerEvent],
predicate: nil
)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
home.addTrigger(trigger) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
logger.info("Automation created: lights off when Andy's door closes.")
}
}
Time-Based Triggers
You can also create automations that fire at specific times. Here is one that sets the hallway to night mode at 9 PM:
extension AndysHouseController {
func createNightModeAutomation(
home: HMHome,
hallwayLight: HMAccessory
) async throws {
guard let brightness = findCharacteristic(
on: hallwayLight,
serviceType: HMServiceTypeLightbulb,
characteristicType: HMCharacteristicTypeBrightness
) else {
throw HomeControllerError.characteristicNotFound
}
var dateComponents = DateComponents()
dateComponents.hour = 21
dateComponents.minute = 0
let timerTrigger = HMTimerTrigger(
name: "Night Mode for Toys",
fireDate: dateComponents.date ?? Date(),
timeZone: .current,
recurrence: dateComponents,
recurrenceCalendar: .current
)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
home.addTrigger(timerTrigger) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
logger.info("Night mode automation created: hallway dims at 9 PM.")
}
}
Note: Automations run on the home hub, not your app. The user needs an Apple TV, HomePod, or iPad configured as a home hub for automations to execute when their iPhone is not on the network.
Matter Integration
Matter is the industry-standard smart home connectivity protocol built on top of Thread and Wi-Fi. Starting with iOS 16.1, HomeKit natively supports Matter accessories. From your app’s perspective, Matter devices look identical to traditional HomeKit accessories once they are commissioned into the home.
Commissioning Matter Devices
Matter device setup uses the same addAndSetupAccessories API, but with a Matter-specific payload:
import HomeKit
@available(iOS 16.1, *)
extension AndysHouseController {
/// Commissions a Matter accessory using its setup payload.
func setupMatterAccessory(in home: HMHome) async throws {
// The system handles Matter commissioning through the standard
// accessory setup flow. Your app provides the topology.
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
home.addAndSetupAccessories { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
}
MatterAddDeviceRequest (iOS 16.4+)
For tighter control over Matter commissioning, use
MatterAddDeviceRequest:
import MatterSupport
@available(iOS 16.4, *)
extension AndysHouseController {
func requestMatterSetup() async throws {
let topology = MatterAddDeviceRequest.Topology(
ecosystemName: "Andy's House",
homes: []
)
let request = MatterAddDeviceRequest(topology: topology)
do {
try await request.perform()
logger.info("Matter device setup completed successfully.")
} catch {
logger.error("Matter setup failed: \(error.localizedDescription)")
throw error
}
}
}
Apple Docs:
MatterAddDeviceRequest— MatterSupport framework
The key insight about Matter from an iOS developer’s perspective: once a Matter device is commissioned into a HomeKit
home, you control it with the exact same HMAccessory, HMService, and HMCharacteristic APIs as any other HomeKit
accessory. The protocol difference is invisible at the API layer.
Thread Network Integration
Many Matter devices use Thread as their networking protocol. Thread is a low-power mesh network that does not depend on Wi-Fi. Apple’s border routers (HomePod mini, Apple TV 4K) automatically create and manage Thread networks. From your app’s perspective, you do not interact with Thread directly — HomeKit handles the routing.
@available(iOS 16.1, *)
extension AndysHouseController {
/// Checks if an accessory is connected via Thread (Matter over Thread).
func isThreadAccessory(_ accessory: HMAccessory) -> Bool {
return accessory.profiles.contains { profile in
profile is HMNetworkConfigurationProfile
}
}
}
Advanced Usage
Grouping Actions with Action Sets
Action sets let you bundle multiple characteristic writes into a single command. This is how “scenes” work in the Home app — “Movie Night” might dim the lights, close the blinds, and set the thermostat:
extension AndysHouseController {
func createMovieNightScene(
home: HMHome,
light: HMAccessory,
thermostat: HMAccessory
) async throws {
let actionSet = try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<HMActionSet, Error>) in
home.addActionSet(withName: "Toy Story Movie Night") { actionSet, error in
if let error {
continuation.resume(throwing: error)
return
}
guard let actionSet else {
continuation.resume(throwing: HomeControllerError.homeCreationFailed)
return
}
continuation.resume(returning: actionSet)
}
}
// Dim lights to 20%.
if let brightness = findCharacteristic(
on: light,
serviceType: HMServiceTypeLightbulb,
characteristicType: HMCharacteristicTypeBrightness
) {
let dimAction = HMCharacteristicWriteAction(
characteristic: brightness,
targetValue: 20 as NSNumber
)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
actionSet.addAction(dimAction) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
// Set thermostat to 22C (~72F).
if let targetTemp = findCharacteristic(
on: thermostat,
serviceType: HMServiceTypeThermostat,
characteristicType: HMCharacteristicTypeTargetTemperature
) {
let tempAction = HMCharacteristicWriteAction(
characteristic: targetTemp,
targetValue: 22 as NSNumber
)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
actionSet.addAction(tempAction) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
logger.info("Movie Night scene created with \(actionSet.actions.count) actions.")
}
/// Executes a named action set (scene).
func executeScene(named name: String, in home: HMHome) async throws {
guard let actionSet = home.actionSets.first(where: { $0.name == name }) else {
throw HomeControllerError.accessoryNotFound
}
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
home.executeActionSet(actionSet) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
logger.info("Executed scene: \(name)")
}
}
Siri and Shortcuts Integration
HomeKit accessories and scenes are automatically available to Siri. The user can say “Hey Siri, activate Toy Story Movie Night” without any additional code from your app. If you want deeper Shortcuts integration, consider pairing HomeKit controls with App Intents to expose custom actions in the Shortcuts app.
Handling Multi-Home Users
Production apps must handle users with multiple homes. Do not assume primaryHome is always set:
extension AndysHouseController {
func safeCurrentHome() throws -> HMHome {
if let primary = homeManager.primaryHome {
return primary
}
guard let first = homeManager.homes.first else {
throw HomeControllerError.noHomesAvailable
}
return first
}
}
Performance Considerations
Local control is fast; remote is not. When the user is on the same network as their home hub, HomeKit commands execute in milliseconds over the local network. When controlling accessories remotely (via iCloud relay), latency can reach 1-3 seconds. Design your UI to handle this gracefully — show optimistic updates with a pending indicator.
Batch characteristic writes. If you need to change multiple characteristics on the same accessory (brightness and color temperature), use an action set or write them in quick succession. Each write requires a round-trip to the accessory, so batching reduces perceived latency.
Cache accessory state sparingly. HomeKit accessories can be controlled by multiple apps, Siri, automations, and physical switches. Any cached state in your app can become stale instantly. Always read the current value before making decisions based on accessory state, or use characteristic notifications to stay updated.
Home structure changes are rare but must be handled. The delegate callbacks for homeManagerDidUpdateHomes,
homeDidUpdateName, and accessory availability changes can fire at any time. Structure your observation layer to handle
these gracefully without full-screen reloads.
Apple Docs: Configuring a Home Automation — HomeKit
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Controlling HomeKit/Matter certified accessories | HomeKit is the right tool. Use HMHomeManager and the standard APIs. |
| Custom BLE device without HomeKit certification | Use Core Bluetooth directly. |
| Cross-platform smart home (Android + iOS) | Matter support makes HomeKit interoperable across platforms. |
| Automation that runs without the app | HomeKit triggers execute on the home hub. Only framework with this. |
| Siri voice control of accessories | HomeKit accessories are automatically Siri-enabled. No code needed. |
| Commercial/enterprise building automation | HomeKit is designed for residential use. Consider dedicated protocols. |
| Accessories that need firmware updates | Use Core Bluetooth for OTA updates. |
Summary
- HomeKit provides a unified API to discover, control, and automate smart home accessories from any manufacturer through
the
HMHome>HMAccessory>HMService>HMCharacteristichierarchy. - Always wait for the
homeManagerDidUpdateHomesdelegate callback before accessing homes. The data loads asynchronously. - Automations (event-based and time-based triggers) execute on the home hub without your app running, making them reliable for real smart home scenarios.
- Matter integration is transparent at the API layer. Once commissioned, Matter accessories use the same
HMAccessoryAPIs as native HomeKit devices. - Use characteristic notifications to observe real-time state changes rather than polling.
If your app needs to communicate with custom BLE hardware that is not HomeKit or Matter certified, check out Core Bluetooth: Connecting to BLE Accessories and IoT Devices for the lower-level approach. For exposing your HomeKit controls as Siri commands and Shortcuts actions, App Intents and Siri covers how to bridge the two frameworks.