Core HID on macOS: Device Discovery, Reports, and Client Lifecycles
Build robust macOS HID integrations with narrow matching, asynchronous device streams, report validation, permission handling, and hot-plug recovery.
Human Interface Devices include more than keyboards and mice. They can be USB or Bluetooth controllers, specialist input panels, sensors, and software-backed peripherals that expose a HID report descriptor. Apple’s Core HID framework provides APIs to discover matching devices and create clients that receive reports or request device state. Treat it as a device protocol boundary, not as a generic guarantee that every peripheral exposes the same fields or is available to every app.
The useful design begins with the smallest device class the product supports. Broad enumeration increases noise, exposes unrelated device metadata, and makes it harder to select the intended peripheral. Matching identifies candidates; it does not prove that a device is compatible with your protocol or that access has been approved.
Match only supported devices
Use HIDDeviceManager.DeviceMatchingCriteria to filter discovery by properties such as usage, vendor ID, product ID, transport, or product. Prefer stable protocol attributes that the device vendor documents. Product names can vary across firmware or localization, and a model name by itself is not a complete protocol version.
import CoreHID
func discoverExampleDevice() async throws {
let manager = HIDDeviceManager()
let criteria = HIDDeviceManager.DeviceMatchingCriteria(
vendorID: 0x1234,
productID: 0x5678
)
for try await notification in await manager.monitorNotifications(
matchingCriteria: [criteria]
) {
switch notification {
case .deviceMatched(let reference):
guard let client = HIDDeviceClient(deviceReference: reference) else {
continue
}
let usage = await client.primaryUsage
recordCandidate(usage: usage)
case .deviceRemoved:
recordRemoval()
default:
break
}
}
}
func recordCandidate(usage: HIDUsage) { }
func recordRemoval() { }
The vendor and product values in this sample are placeholders, not identifiers for a real accessory. Replace them with values from the supported device specification and still validate the device’s usage, transport, descriptor, and required report layout after matching. A discovery result is a reference to a candidate, not a completed pairing or an authentication result.
The manager’s asynchronous stream reports devices already matching when monitoring begins and later matching additions or removals. Keep the task that consumes it under an explicit owner. When a document, window, or feature closes, cancel that task and release any client it created. If reconnection should be automatic, model it as a state machine with a generation ID so delayed work from a removed device cannot update a newer connection.
Separate discovery from an active client
Create a HIDDeviceClient from a device reference only after deciding that the candidate’s properties match the product’s supported protocol. A client provides access to device attributes, report descriptors, elements, and asynchronous input or report operations. Read only the data required for the feature; serial numbers and location identifiers can be sensitive or unstable and should not become general analytics keys.
Device removal, seizure by another client, permission changes, and transport loss are separate events. Handle the terminal event and cancel dependent work. A reference can become stale between discovery and client creation, so failure to create a client is an expected lifecycle outcome rather than a fatal app error.
Core HID’s actor-based manager and client help isolate API operations, but an actor does not make a device’s physical state durable. The peripheral can disconnect while a request is pending. Give each operation a deadline, handle errors and cancellation, and re-query a device before sending a later command that depends on a previous state.
Treat reports as a protocol, not a key event
HID input reports are byte sequences described by a report descriptor. The descriptor defines usages, report IDs, field widths, logical ranges, and collections. Parse reports according to the actual descriptor and protocol version. Do not assume that byte offsets, signedness, units, or report sizes are identical across devices that happen to share a marketing name.
Validate report length, report ID, element ranges, and state transitions before changing application state. Cap any allocation based on device-supplied lengths, reject truncated or unexpected reports, and treat unknown fields as unsupported rather than guessing. A single physical gesture can result in multiple element updates; design the UI around normalized application events rather than one callback equaling one user action.
The high-level HIDElement representation can be appropriate when the app needs individual values. Raw report monitoring is useful when the accessory protocol is defined in terms of reports. Choose one interpretation layer, document the mapping from device values to app actions, and test it against captured fixtures from each supported hardware revision.
Permissions and product boundaries
Apple notes that interacting with certain HID classes, including keyboards, requires user approval. Ask only when the person initiates a feature that needs the access, describe why it is needed, and show a clear fallback if access is denied or restricted. Do not interpret a device match as proof that access to its input data will succeed.
Use the higher-level Game Controller framework for game-controller behavior when it provides the abstractions the product needs. Raw HID exposes lower-level descriptors and requires device-specific handling. Avoid treating internal, virtual, or compatibility devices as if they were necessarily physical accessories. Keep the supported device scope narrow and test system-created devices separately.
Device identity is not user identity. A vendor ID, product ID, or serial string must not be used as an account credential. If the app sends input data to a service, obtain the required user consent, minimize the data, and protect it under the app’s normal privacy and transport policy. Most integrations can keep raw reports local and convert them into a small semantic event.
Backpressure, latency, and teardown
Input may arrive faster than the UI can redraw. Process reports on the stream’s execution context, normalize them quickly, and coalesce only values for which intermediate states are not meaningful. Do not block a device callback while performing disk or network I/O. For button transitions or ordered control messages, preserve ordering and ensure a dropped update cannot leave the app stuck in a pressed state.
Keep a client-to-device session object that owns the client, monitoring task, current device generation, and teardown logic. Removal should stop input consumption, clear any pressed or active state, and release resources once. Repeated connection and removal events should not create duplicate observers or duplicate commands. Ensure a late task result checks the generation before it mutates the model.
For requests that change a device setting, distinguish command acceptance from observed state. A successful request return does not always prove that a physical device applied the value. Read back the relevant state when supported, or expose the feature as best effort and report timeout or unsupported capability precisely.
Descriptor evolution and protocol fixtures
The report descriptor is part of the device’s protocol description. Store test fixtures for each supported hardware and firmware revision, and compare the descriptor or normalized element inventory when a vendor changes firmware. Do not assume an update preserves report IDs merely because the product identifier did not change. If the descriptor differs, route to a known parser version or report the device as unsupported rather than reading offsets based on an obsolete fixture.
For values represented as HIDElement, inspect the element’s usage, logical range, unit, and report association before converting it into a product value. A value may need normalization, debouncing, or calibration. Keep calibration tied to an explicit device or user profile and avoid changing the device’s global state when a local display transform is sufficient.
Device names and serial numbers can be absent or duplicated. Choose an identity suitable for the current connection, such as a fresh session generation plus the available transport and product attributes. If persistence across reconnects is required, make that a user-approved feature with a documented privacy reason. Do not use a serial number as an implicit account link or cross-site tracking ID.
Choosing Core HID versus a higher-level framework
Core HID is appropriate when an app must reason about HID usages, report descriptors, or a specific accessory protocol. If the feature only needs standard game-controller buttons and axes, the Game Controller framework may provide a more portable semantic model. If the feature manages hardware that needs a dedicated driver or system extension, an app-level HID client is not a substitute for the relevant DriverKit design.
Avoid opening a device and requesting reports just to identify it when public matching metadata is sufficient. A read request can wake hardware, take longer than enumeration, or fail under access policy. Keep discovery side effects low and delay active communication until the user has selected a supported device or the feature has an explicit device-selection rule.
Validation matrix
Test a matching device present at startup, attachment after monitoring begins, removal during an input stream, duplicate devices with the same product, unknown firmware, missing report IDs, truncated data, a device seized by another client, denied approval, and reconnect while old work is pending. Include USB and Bluetooth transport if both are supported. Use Apple’s virtual HID facilities for deterministic development tests where the feature and environment permit them, then verify the real target hardware separately.
Record the matching criteria, normalized device properties, transport, report type, connection generation, and error category. Avoid recording raw user keystrokes, unbounded report payloads, or stable hardware identifiers unless the feature truly needs them. Diagnostics should make it possible to distinguish discovery, access, protocol parsing, and hardware-response failures.
Core HID supplies discovery and communication primitives. The application still owns device scope, permission messaging, report parsing, removal recovery, privacy, and user-visible meaning. Match narrowly, validate every report, and keep physical connection state separate from the application’s own workflow state.
Related:
- Game Controller on macOS: Device Discovery, Profiles, and Input Ownership
- DriverKit on macOS: Hardware Drivers That Run in User Space
Sources: