Receiving iOS notifications on an accessory
Create custom app extensions that manage iOS system notifications for your accessory.
Overview
When someone opts into notification forwarding for your accessory from their iPhone, the system identifies your accessory with the reference your companion app receives from AccessorySetupKit. The system prompts the person for permission to forward notifications to the accessory from the apps that they choose in the prompt.
Implement three extensions using the Accessory Notifications framework to forward notifications securely:
- AccessoryDataProvider
Receives notification content and prepares it for transmission.
- AccessoryTransportSecurity
Manages cryptographic key exchange with your accessory.
- AccessoryTransportAppExtension
Relays encrypted data to your accessory over Bluetooth.
The system coordinates these extensions, encrypting notification data before transmission so that only your accessory can decrypt it.
Your accessory receives the notification data and decrypts it using HPKE (RFC9180) before parsing the notification details. Extract the notification properties such as title, subtitle, body, and any rich content you include in the transmission.
Alert for the notification on your accessory by presenting it on screen, playing a sound, or triggering a haptic effect that uses touch to give users feedback. You can include a suggested alerting strategy in your transmission based on particular situations or hints the system provides.
Register for notification forwarding
To register your accessory’s companion app for notification forwarding, call the AccessoryNotificationCenter class’s requestForwarding(for:) method:
import AccessoryNotifications
import AccessorySetupKit
// Register the accessory with AccessorySetupKit.
let accessory: ASAccessory = /* ... */
// Prompt for permission to opt into notification forwarding.
let center = AccessoryNotificationCenter()
let result = try await center.requestForwarding(for: accessory)The system prompts the person for permission to forward notifications and allows them to select the apps on their device that can provide notifications. When the person finishes interacting with the UI and dismisses the prompt, the requestForwarding(for:) method returns the person’s choice in the ForwardingDecision result. The ForwardingDecision.allow value indicates that the person allows your accessory to receive notifications from all applicable apps. If the result is ForwardingDecision.limited, your accessory can receive notifications from a subset of apps. The other decision types indicate the person doesn’t opt into notification forwarding.
Create an extension to receive notifications
To receive notification content, create an AccessoryDataProvider extension in your accessory’s companion app by adding a new target to your Xcode project with the app extension type. In the extension’s target properties, specify the extension point identifier com.apple.accessory-data-provider and declare a capability for AccessoryNotifications.NotificationsForwarding:
<plist>
<dict>
<key>EXAppExtensionAttributes</key>
<dict>
<key>EXExtensionPointIdentifier</key>
<string>com.apple.accessory-data-provider</string>
<key>EXCapabilities</key>
<array>
<string>AccessoryNotifications.NotificationsForwarding</string>
</array>
</dict>
</dict>
</plist>In your extension’s code, implement the AccessoryDataProvider protocol and provide a handler that conforms to NotificationsForwarding.AccessoryNotificationsHandler:
import AccessoryNotifications
import AccessoryTransportExtension
@main
struct DataProvider: AccessoryDataProvider {
var extensionPoint: AppExtensionPoint {
Identifier("com.apple.accessory-data-provider")
Implementing {
NotificationsForwarding {
NotificationHandler()
}
}
}
}
// Responds to system-related notification requests.
class NotificationHandler: AccessoryNotificationsHandler {
var session: NotificationsForwarding.Session?
func activate(for session: NotificationsForwarding.Session) {
self.session = session
}
func addNotification(_ notification: AccessoryNotification,
alertingContext: AlertingContext) async throws -> Bool {
// Curate the notification details for your accessory.
}
func updateNotification(_ notification: AccessoryNotification) {
// Accommodate updated notification data.
}
func removeNotification(identifier: AccessoryNotification.Identifier) {
// Remove a previously displayed notification.
}
func removeAllNotifications() {
// Remove all notifications.
}
func messageHandler(_ message: AccessoryMessage) {
// Handle messages from the accessory.
}
}The system requires your app extension to have the com.apple.developer.accessory-data-provider entitlement to use the AccessoryDataProvider protocol.
Receive and process notifications
When a notification occurs on the iPhone, the system invokes your extension by calling doc://com.apple.documentation/documentation/accessorynotifications/notificationsforwarding/accessorynotificationshandler/activate(for:), passing in a session object. Save a reference to the session for use across multiple notifications.
The system then calls addNotification(_:alertingContext:) on your extension, passing in the notification’s details. Parse the <doc://com.apple.documentation/documentation/accessorynotifications/accessorynotification`` structure, selecting just the information your accessory needs. Notification details include:
- Display content
title, subtitle, and summary (for Apple Intelligence summaries)
- Rich elements
sourceIcon, contextIcon, attachments, and <doc://com.apple.documentation/documentation/accessorynotifications/accessorynotification/body``, which can contain a genmoji through the NSAdaptiveImageGlyph class
- Interactive components
actions array
- Metadata
identifier, sourceName, threadIdentifier, deliveryDate, and displayDate
- Priority attributes
attributes for critical, time-sensitive, or priority notifications
Serialize the notification details you select and create an AccessoryMessage to send the curated data to your accessory:
func addNotification(_ notification: AccessoryNotification,
alertingContext: AlertingContext) async throws -> Bool {
// Check if the notification needs to alert.
guard alertingContext.shouldAlert else {
return false
}
// Extract and serialize notification data.
let notificationData = serializeNotification(notification)
// Create a message payload.
let message = AccessoryMessage {
AccessoryMessage.Payload(transport: .bluetooth, data: notificationData)
}
// Send the message payload to your accessory.
try await session?.send(message: message)
// Return true to indicate successful alerting.
return true
}
// Chooses fields the accessory supports and implements a custom binary format.
func serializeNotification(_ notification: AccessoryNotification) -> Data {
var data = Data()
// Add title, subtitle, body, and so on.
return data
}You can send one message per notification, or you can partition the data for a single notification into multiple payloads. If Bluetooth is unavailable, the system delivers the message on any transport currently available.
The method returns a Boolean value indicating whether your accessory alerted for the notification. Return true if the accessory successfully alerts the person, or false otherwise. This information helps the system coordinate alerting across multiple devices.
Create an extension to send notifications securely
Create an AccessoryTransportSecurity extension to manage the cryptographic key exchange process. Add a new extension target to your Xcode project. In the extension’s target properties, specify the extension point identifier com.apple.accessory-transport-security:
<plist>
<dict>
<key>EXAppExtensionAttributes</key>
<dict>
<key>EXExtensionPointIdentifier</key>
<string>com.apple.accessory-transport-security</string>
</dict>
</dict>
</plist>In your extension’s code, implement the AccessoryTransportSecurity protocol, in which you accept(_:) incoming security session requests for your accessory:
import AccessoryTransportExtension
import CryptoKit
@main
struct TransportSecurity: AccessoryTransportSecurity {
@AppExtensionPoint.Bind
static var boundExtensionPoint: AppExtensionPoint {
Identifier("com.apple.accessory-transport-security")
}
func accept(sessionRequest: AccessorySecuritySession.Request) -> AccessorySecuritySession.Request.Decision {
return sessionRequest.accept {
SecurityEventHandler(session: sessionRequest.session)
}
}
}The system requires your app extension to have the com.apple.developer.accessory-transport-security entitlement to use the AccessoryTransportSecurity protocol.
Provide a security event handler
The security extension assists with cryptography by handling key exchange messages between your accessory and the system. Provide an event handler that conforms to the AccessorySecuritySession class’s AccessorySecuritySession.EventHandler protocol. The system calls your handler’s messageReceived(_:completion:) method with security messages during the key exchange:
class SecurityEventHandler: AccessorySecuritySession.EventHandler {
private var session: AccessorySecuritySession
private var keyMaterial: SecurityMessage?
private var publicKeyData: Data?
private var privateKeyData: Data?
init(session: AccessorySecuritySession) {
self.session = session
}
func messageReceived(_ message: SecurityMessage,
completion: @escaping @Sendable (AccessoryMessage.Result) -> Void) {
switch message.keyType {
case .encapsulatedKey:
handleKeyExchange(message: message, completion: completion)
default:
completion(.success)
}
}
// Cleans up key material.
func sessionInvalidated(error: (any Error)?) {
keyMaterial = nil
privateKeyData = nil
publicKeyData = nil
}
}Initiate key exchange from your accessory
Your accessory initiates the key exchange process when it’s ready to establish encrypted communication. Generate a public-private key pair on your accessory and send the public key to the system. Choose SecurityMessage.CipherSuite.xWing for post-quantum security, or SecurityMessage.CipherSuite.p256 as a fallback if your accessory doesn’t support XWing:
func handleKeyRequest(cipherSuite: SecurityMessage.CipherSuite) {
do {
let accessoryPublicKey: Data
let accessoryPrivateKey: Data
switch cipherSuite {
case .xWing:
// Generate an XWing key pair.
let privateKey = try XWingMLKEM768X25519.PrivateKey()
accessoryPrivateKey = privateKey.seedRepresentation
accessoryPublicKey = privateKey.publicKey.rawRepresentation
case .p256:
// Generate a P256 key pair.
let privateKey = P256.KeyAgreement.PrivateKey()
accessoryPrivateKey = privateKey.rawRepresentation
accessoryPublicKey = privateKey.publicKey.rawRepresentation
}
privateKeyData = accessoryPrivateKey
publicKeyData = accessoryPublicKey
// Send public key to the system.
let message = SecurityMessage(
keyType: .publicKey,
cipherSuite: cipherSuite,
version: .version1,
key: accessoryPublicKey,
supportedTransports: [.bluetooth]
)
try session.sendSecurityMessage(message)
} catch {
session.cancel(error: error)
}
}To use AccessoryTransport.internet or AccessoryTransport.localNetwork transports, you must use XWing for enhanced security. Specify the supported transports in the supportedTransports parameter.
Complete the key exchange
The system generates cryptographic key material and sends it to your extension by invoking your handler’s messageReceived(_:completion:) method with a SecurityMessage that has a SecurityMessage.KeyType.encapsulatedKey key type. Forward this key material to your accessory via Bluetooth and call the completion handler:
func handleKeyExchange(message: SecurityMessage,
completion: @escaping @Sendable (AccessoryMessage.Result) -> Void) {
guard let privateKeyData = privateKeyData,
let publicKeyData = publicKeyData else {
completion(.failure(.transportFailed))
return
}
self.keyMaterial = message
do {
// Send key material to the accessory via Bluetooth.
sendKeyMaterialToAccessory(message)
// Inform the system of successful transmission.
completion(.success)
} catch {
session.cancel(error: error)
completion(.failure(.transportFailed))
}
}The completion handler is required. Call it with .success if the key material successfully transmits to your accessory, or .failure(.transportFailed) if transmission fails but may recover. If you don’t call the completion handler, the system assumes successful delivery.
Implement an extension to relay encrypted notifications
To send the encrypted data to your accessory, use an AccessoryTransportAppExtension. Implement the messageReceived(_:completion:) method in your event processing code, and the system calls your handler to transmit encrypted data for each message payload:
class TransportEventHandler: AccessoryTransportSession.EventHandler {
func messageReceived(_ message: TransportMessage,
completion: @escaping @Sendable (AccessoryMessage.Result) -> Void) {
do {
// Transmit encrypted notification data to the accessory over Bluetooth.
try sendToAccessory(message.data, sessionID: message.sessionID)
completion(.success)
} catch {
// Transport failed but may recover.
completion(.failure(.transportFailed))
}
}
// Cleans up when the session ends.
func sessionInvalidated(error: (any Error)?) {
// Clean up connection state.
}
}The system encrypts notification data using keys exchanged through your AccessoryTransportSecurity extension before delivering it to your handler. Because the data is encrypted, your extension is unable to read or otherwise make sense of the data, and can only transmit it.
Call the completion handler with .success if the message transmits successfully, .failure(.transportFailed) if the transport fails but may recover, or .failure(.transportUnavailable) if the transport is unavailable. The system retries failed messages or attempts delivery on a different transport.
Add the com.apple.developer.accessory-transport-extension entitlement to your extension’s code signature to use the AccessoryTransportAppExtension protocol.
Decrypt notification data on your accessory
When your accessory receives the encrypted notification data, it decrypts the data using HPKE (RFC9180) with keys exchanged through the transport security extension.
The accessory and extension share a secret, that is, a custom string phrase, for the HPKE decryption algorithm. Build the string using information about the protocol and key material:
let cipherSuite = securityMessage.cipherSuite.description // The value is "XWing" or "P256".
let version = securityMessage.version.description
let identifier = securityMessage.identifier ?? deviceUUID // The CBPeripheral UUID.
let protocolInfo = Data("\(cipherSuite)-\(version)-\(identifier)".utf8)Create an HPKE receiver from the accessory’s private key and protocol information. The following code uses the SecurityMessage.CipherSuite.xWing cipher suite:
let publicKey = try XWingMLKEM768X25519.PublicKey(rawRepresentation: accessoryPublicKeyData)
let privateKey = try XWingMLKEM768X25519.PrivateKey(
seedRepresentation: accessoryPrivateKeyData,
publicKey: publicKey
)
let recipient = try HPKE.Recipient(
privateKey: privateKey,
ciphersuite: .XWingMLKEM768X25519_SHA256_AES_GCM_256,
info: protocolInfo,
encapsulatedKey: encapsulatedKeyFromSystem
)Alternatively, the following code creates an HPKE receiver using the fallback SecurityMessage.CipherSuite.p256 cipher suite:
let privateKey = try P256.KeyAgreement.PrivateKey(rawRepresentation: accessoryPrivateKeyData)
let recipient = try HPKE.Recipient(
privateKey: privateKey,
ciphersuite: .P256_SHA256_AES_GCM_256,
info: protocolInfo,
encapsulatedKey: encapsulatedKeyFromSystem
)Derive a notification forwarding-specific secret by appending the direction and feature ID to the protocol information. The feature ID is the session ID of the transport message that you receive from messageReceived(_:completion:).
let featureID = // The transport message's session ID.
let context = Data("\(protocolInfo)-HostToAccessory-\(featureID)".utf8)
let secret = try recipient.exportSecret(context: context, outputByteCount: 32)The direction is HostToAccessory for data that flows from the iPhone to the accessory, and AccessoryToHost for data that flows from the accessory to the iPhone.
The system encrypts notification data using AES-GCM as specified in NIST Special Publication 800-38D. The ciphertext on the wire encodes as:
IV (12 bytes) || ciphertext || MAC (16 bytes)Decrypt the data using the feature-specific secret:
let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
let plaintext = try AES.GCM.open(sealedBox, using: secret)If decryption fails, close the connection, reset the encryption state, and resynchronize the key exchange. If decryption succeeds, parse the notification information according to your accessory’s custom format.
Alert someone about a notification
To alert for a notification, present it on screen, play a sound, or trigger a haptic effect that uses touch to give users feedback on your accessory device.
Use the AlertingContext to determine whether a notification requires an alert. The system provides the shouldAlert property, which represents the person’s preferred notification behavior using notification settings and the iOS device’s current Focus state.
The notificationCanAlert property indicates whether the notification has sound and alert permissions. The system might set notificationCanAlert to false when the notification already alerts on another device or if device settings disable alerting for the notification.
The isSuppressedByFocus property indicates whether the device’s Focus state suppresses notification alerts.
For incoming call notifications, check doc://com.apple.documentation/documentation/accessorynotifications/AlertingContext/isIncomingCall to apply special handling. Use sound to determine sound characteristics, including whether the notification should ignore silent mode with shouldIgnoreSilentMode.
Handle notification updates and removals
The forwarding life cycle includes requests to update a notification after your accessory receives it, or to remove one or more existing notifications.
When a notification’s content changes, the system notifies your extension by calling updateNotification(_:). Update the notification on your accessory without alerting again:
func updateNotification(_ notification: AccessoryNotification) {
let notificationData = serializeNotification(notification)
let message = AccessoryMessage {
AccessoryMessage.Payload(transport: .bluetooth, data: notificationData)
}
Task {
try await session?.send(message: message)
}
}If someone dismisses a notification on another device after your accessory receives the notification, the system follows up with a removal request by calling removeNotification(identifier:):
// Requests the removal of a notification from the accessory.
func removeNotification(identifier: AccessoryNotification.Identifier) {
let removalData = serializeRemoval(identifier)
let message = AccessoryMessage {
AccessoryMessage.Payload(transport: .bluetooth, data: removalData)
}
Task {
try await session?.send(message: message)
}
}The system calls removeAllNotifications() when your accessory needs to remove all notifications, such as when the person deletes the app that sent the notifications:
// Requests the removal of all notifications from the accessory.
func removeAllNotifications() {
let clearData = serializeClearAll()
let message = AccessoryMessage {
AccessoryMessage.Payload(transport: .bluetooth, data: clearData)
}
Task {
try await session?.send(message: message)
}
}