CBPeripheral è una classe del framework Core Bluetooth che rappresenta un dispositivo BLE remoto su iOS. Ogni oggetto CBPeripheral incapsula UUID, nome, RSSI e la gerarchia dei servizi GATT di un dispositivo BLE connesso. Lo sviluppatore interagisce con la periferica esclusivamente tramite CBPeripheral: scoperta dei servizi (discoverServices:), lettura delle caratteristiche (readValueForCharacteristic:), scrittura dei dati (writeValue:forCharacteristic:type:) e sottoscrizione alle notifiche (setNotifyValue:forCharacteristic:). Secondo Apple Developer, 2026, CBPeripheral è l'oggetto centrale per tutte le operazioni con periferiche BLE, restituito da CBCentralManager alla scoperta o connessione di un dispositivo.
Punti chiave
CBPeripheral è un oggetto che rappresenta un dispositivo BLE remoto in un'applicazione iOS. A differenza di CBCentralManager, che gestisce l'adattatore Bluetooth locale dell'iPhone, CBPeripheral modella un dispositivo periferico esterno: un sensore, un fitness tracker, un beacon o uno strumento medico. Ogni istanza di CBPeripheral contiene un identificatore univoco (UUID) che persiste tra le sessioni di connessione — Apple collega l'UUID a un dispositivo specifico tramite il Bonding di sistema.
CBPeripheral non viene creato direttamente tramite init. Il framework Core Bluetooth restituisce un oggetto CBPeripheral in due scenari: quando un dispositivo viene scoperto tramite scanForPeripheralsWithServices: (delegato didDiscoverPeripheral) e quando ci si connette a un dispositivo precedentemente noto tramite retrievePeripheralsWithIdentifiers:. Dopo aver ottenuto l'oggetto, lo sviluppatore chiama connectPeripheral: su CBCentralManager, dopodiché CBPeripheral diventa disponibile per le operazioni GATT.
Ciclo di vita di CBPeripheral include sei stati: disconnesso (iniziale), connessione in corso (dopo la chiamata a connect), connesso (dopo didConnectPeripheral), scoperta in corso (durante la chiamata a discoverServices), scoperto (dopo aver ricevuto i servizi) e disconnessione in corso (dopo cancelPeripheralConnection). Ogni stato viene tracciato tramite il protocollo delegato CBPeripheralDelegate — indispensabile per qualsiasi applicazione BLE su iOS.
CBPeripheral memorizza una struttura gerarchica GATT composta da tre livelli. Il livello radice è un array di CBService (servizi), ogni servizio contiene un array di CBCharacteristic (caratteristiche), ogni caratteristica contiene un array di CBDescriptor (descrittori). Questo modello è pienamente conforme alla specifica Bluetooth GATT: un servizio è una funzione del dispositivo (ad esempio, “Heart Rate Service”), una caratteristica è un valore specifico (polso 72 bpm), un descrittore sono metadati della caratteristica (unità di misura, configurazione notifiche).
| Livello | Classe Core Bluetooth | Descrizione |
|---|---|---|
| Servizio | CBService | Gruppo logico di caratteristiche correlate, identificato da UUID (16-bit, 32-bit o 128-bit) |
| Caratteristica | CBCharacteristic | Valore dati specifico, supporta lettura, scrittura e notifiche |
| Descrittore | CBDescriptor | Metadati della caratteristica: configurazione client CCCD, descrizione utente, formato presentazione |
I servizi BLE standard sono registrati da Bluetooth SIG: Heart Rate Service (UUID 180D), Battery Service (180F), Device Information (180A), Blood Pressure (1810). Per servizi personalizzati si utilizzano UUID a 128 bit (ad esempio, E20A39F4-73F5-4BC4-A12F-17D1AD07A961). iOS riconosce automaticamente gli UUID standard e mostra nomi leggibili; gli UUID personalizzati appaiono in formato esadecimale.
Dopo la connessione, la gerarchia di CBPeripheral è vuota — servizi e caratteristiche non vengono caricati. Lo sviluppatore deve chiamare discoverServices: per ottenere i servizi e poi per ogni servizio chiamare discoverCharacteristics:forService:. Se il servizio contiene servizi inclusi, chiamare inoltre discoverIncludedServices:forService:. Solo dopo il completamento della scoperta della gerarchia, CBPeripheral viene popolato e diventa disponibile per lettura e scrittura.
Scoperta della struttura GATT di CBPeripheral è un passaggio obbligatorio prima di qualsiasi operazione di lettura o scrittura. Il metodo discoverServices: avvia una ricerca asincrona di tutti i servizi del dispositivo. Se viene passato nil, vengono scoperti tutti i servizi; se viene passato un array di CBUUID — solo i servizi con gli UUID specificati (ottimizzazione del tempo). Il risultato arriva al delegato peripheral:didDiscoverServices: — l'oggetto CBPeripheral popola la sua proprietà services con un array di CBService.
Dopo aver ricevuto i servizi, per ogni CBService è necessario chiamare discoverCharacteristics:forService:. Analogamente, nil — tutte le caratteristiche, array di CBUUID — solo quelle specificate. Il risultato: peripheral:didDiscoverCharacteristicsForService:error:. In questa fase, CBCharacteristic riceve le proprietà (properties: .read, .write, .notify, .indicate) che definiscono le operazioni consentite.
import CoreBluetooth
extension BLEViewController: CBPeripheralDelegate {
// 1. Service discovery
func peripheral(_ peripheral: CBPeripheral,
didDiscoverServices error: Error?) {
guard let services = peripheral.services else { return }
for service in services {
// Request characteristics for each service
peripheral.discoverCharacteristics(nil, for: service)
}
}
// 2. Characteristic discovery
func peripheral(_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?) {
guard let characteristics = service.characteristics else { return }
for characteristic in characteristics {
if characteristic.properties.contains(.read) {
peripheral.readValue(for: characteristic)
}
if characteristic.properties.contains(.notify) {
peripheral.setNotifyValue(true, for: characteristic)
}
}
}
// 3. Read value
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
guard let data = characteristic.value,
let value = String(data: data, encoding: .utf8)
else { return }
print("Characteristic value: \(value)")
}
}
Nell'esempio, CBPeripheralDelegate implementa tre metodi di scoperta obbligatori. didDiscoverServices itera su tutti i servizi trovati e richiede le caratteristiche. didDiscoverCharacteristicsForService verifica le proprietà di ogni caratteristica: per .read chiama readValue, per .notify chiama setNotifyValue(true). Il metodo didUpdateValueForCharacteristic riceve il valore effettivo in formato Data.
Lettura dei valori di CBCharacteristic viene eseguita utilizzando il metodo readValueForCharacteristic:. Il risultato arriva in modo asincrono in peripheral:didUpdateValueForCharacteristic:error:. Importante: il dispositivo può avere un valore in cache (characteristic.value è disponibile immediatamente dopo la scoperta), ma per ottenere i dati correnti, la chiamata a readValue è obbligatoria. iOS può memorizzare nella cache i valori per efficienza energetica — readValue aggiorna la cache.
Scrittura dei valori viene eseguita utilizzando il metodo writeValue:forCharacteristic:type:. Il parametro type determina il tipo di scrittura: .withResponse (CBCharacteristicWriteWithResponse) — il dispositivo conferma la scrittura tramite didWriteValueForCharacteristic; .withoutResponse (CBCharacteristicWriteWithoutResponse) — scrittura senza conferma, velocità massima ma senza garanzia di consegna. La specifica BLE limita l'MTU (Maximum Transmission Unit): fino a 23 byte per BLE 4.0, fino a 251 byte per BLE 5.0+. Per dati più grandi dell'MTU, è necessaria la frammentazione a livello applicativo.
// CBPeripheral characteristic read and write
class BLEService {
private let peripheral: CBPeripheral
private let serviceUUID = CBUUID(string: "180D")
private let charUUID = CBUUID(string: "2A37")
init(peripheral: CBPeripheral) {
self.peripheral = peripheral
}
// Read with response
func readHeartRate() {
guard let service = peripheral.services?.first(where: { $0.uuid == serviceUUID }),
let characteristic = service.characteristics?.first(where: { $0.uuid == charUUID })
else { return }
peripheral.readValue(for: characteristic)
}
// Write with response (withResponse)
func writeWithResponse(data: Data) {
guard let characteristic = findCharacteristic() else { return }
peripheral.writeValue(data, for: characteristic,
type: .withResponse)
}
// Write without response (withoutResponse)
// Max throughput, no delivery guarantee
func writeWithoutResponse(data: Data) {
guard let characteristic = findCharacteristic() else { return }
peripheral.writeValue(data, for: characteristic,
type: .withoutResponse)
}
private func findCharacteristic() -> CBCharacteristic? {
return peripheral.services?
.flatMap { $0.characteristics ?? [] }
.first(where: { $0.uuid == charUUID })
}
}
La scelta del tipo di scrittura withResponse o withoutResponse dipende dai requisiti di affidabilità. Per i comandi (accendere la luce, aprire la serratura) utilizzare withResponse — la garanzia di consegna è critica. Per i dati in streaming (polso, temperatura) utilizzare withoutResponse — la perdita di un pacchetto è irrilevante. Un dispositivo BLE può supportare un solo tipo di scrittura — verificare le proprietà characteristic.properties.contains(.write) e .writeWithoutResponse.
Notifiche sono un meccanismo BLE in cui il dispositivo periferico invia i valori delle caratteristiche al dispositivo centrale in modo asincrono, senza polling costante da parte del centrale. CBPeripheral attiva la sottoscrizione tramite il metodo setNotifyValue:forCharacteristic:. Dopo l'attivazione della sottoscrizione, iOS scrive automaticamente nel CCCD (Client Characteristic Configuration Descriptor) della periferica e il dispositivo inizia a inviare aggiornamenti ogni volta che il valore cambia.
A differenza delle indicazioni, le notifiche non richiedono conferma dal dispositivo centrale — il pacchetto viene inviato e dimenticato. Ciò fornisce il massimo throughput, ma è possibile la perdita di pacchetti. Le indicazioni richiedono conferma a livello di protocollo (L2CAP) — più affidabili ma più lente. La proprietà properties di CBCharacteristic indica esattamente quale modalità è supportata: .notify, .indicate o entrambe.
Quando CBPeripheral si disconnette (disconnessione, fuori portata), tutte le sottoscrizioni attive vengono automaticamente reimpostate. Al momento della riconnessione, è necessario chiamare nuovamente setNotifyValue:true per ogni caratteristica. iOS perde anche le sottoscrizioni quando l'applicazione esce dal primo piano (se la modalità background non è attivata) — per il funzionamento in background, è necessario abilitare la capacità “Uses Bluetooth LE accessories” in Info.plist.
// CBPeripheral notification subscription management
class NotificationManager: NSObject {
private var peripheral: CBPeripheral?
private var subscribedCharacteristics: Set<CBUUID> = []
// Subscribe to notifications for all .notify characteristics
func subscribeToAllNotifications(peripheral: CBPeripheral) {
self.peripheral = peripheral
guard let services = peripheral.services else { return }
for service in services {
guard let characteristics = service.characteristics else { continue }
for characteristic in characteristics {
if characteristic.properties.contains(.notify)
|| characteristic.properties.contains(.indicate) {
peripheral.setNotifyValue(true, for: characteristic)
subscribedCharacteristics.insert(characteristic.uuid)
}
}
}
}
// Unsubscribe from all notifications
func unsubscribeFromAll() {
guard let peripheral = peripheral else { return }
guard let services = peripheral.services else { return }
for service in services {
guard let characteristics = service.characteristics else { continue }
for characteristic in characteristics {
if subscribedCharacteristics.contains(characteristic.uuid) {
peripheral.setNotifyValue(false, for: characteristic)
}
}
}
subscribedCharacteristics.removeAll()
}
// Notification handler
func peripheral(_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?) {
if characteristic.isNotifying {
print("Subscription active: \(characteristic.uuid)")
} else {
print("Subscription inactive: \(characteristic.uuid)")
}
}
}
Il NotificationManager dimostra la corretta gestione delle notifiche CBPeripheral. subscribeToAllNotifications itera su tutti i servizi e caratteristiche, attivando .notify e .indicate. subscribedCharacteristics tiene traccia delle sottoscrizioni attive per una corretta disiscrizione. didUpdateNotificationStateForCharacteristic conferma il cambiamento riuscito dello stato di sottoscrizione tramite la proprietà characteristic.isNotifying.
Flusso di lavoro completo con CBPeripheral include: ottenimento dell'oggetto da CBCentralManager, connessione, scoperta, lettura/scrittura, sottoscrizione alle notifiche e disconnessione. L'esempio seguente implementa una classe BLEConnection che gestisce l'intero ciclo di vita di una periferica BLE in Swift utilizzando l'API moderna async/await (iOS 15+).
import CoreBluetooth
// Full CBPeripheral management example with async/await
class BLEConnection: NSObject {
private let centralManager: CBCentralManager
private var peripheral: CBPeripheral?
private var continuation: CheckedContinuation<Void, Error>?
override init() {
centralManager = CBCentralManager(delegate: nil, queue: .main)
super.init()
centralManager.delegate = self
}
// 1. Connect to peripheral
func connect(to peripheral: CBPeripheral) async throws {
self.peripheral = peripheral
peripheral.delegate = self
centralManager.connect(peripheral, options: nil)
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
}
}
// 2. Discovery
func discoverServices() async throws {
guard let peripheral = peripheral else {
throw BLEError.notConnected
}
peripheral.discoverServices(nil)
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
}
}
}
// 3. CBCentralManager
extension BLEConnection: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
// Handle Bluetooth device state
}
func centralManager(_ central: CBCentralManager,
didConnect peripheral: CBPeripheral) {
continuation?.resume()
continuation = nil
}
func centralManager(_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral,
error: Error?) {
continuation?.resume(throwing: error ?? BLEError.connectionFailed)
continuation = nil
}
}
enum BLEError: Error {
case notConnected
case connectionFailed
case serviceNotFound
case characteristicNotFound
}
La classe BLEConnection utilizza Swift Concurrency (async/await) tramite CheckedContinuation — un pattern moderno per lavorare con API delegate di Core Bluetooth. connect(to:) attende la conferma della connessione tramite didConnectPeripheral, discoverServices() — tramite didDiscoverServices. Questo approccio elimina i delegati annidati e rende il codice BLE lineare e leggibile. La gestione degli errori tramite BLEError copre tutti gli scenari tipici di fallimento della connessione BLE.
Domande frequenti
CBPeripheral per un dispositivo precedentemente connesso può essere ottenuto tramite retrievePeripheralsWithIdentifiers: su CBCentralManager. Passare un array di UUID (NSUUID) di dispositivi precedentemente salvati — il framework restituisce un array di CBPeripheral per i dispositivi nel database di bonding BLE di sistema. Funziona solo per dispositivi con cui l'iPhone è stato precedentemente accoppiato. Per un nuovo dispositivo, la scansione è obbligatoria.
Cause comuni: il dispositivo è fuori portata (RSSI sotto la soglia), la radio BLE è spenta (CBCentralManager.state != .poweredOn), il delegato CBPeripheralDelegate non è impostato (peripheral.delegate = self), o discoverServices è stato chiamato prima della connessione. Verificare centralManager.state, assicurarsi che il delegato sia impostato prima di chiamare connect e utilizzare un tentativo con timeout di 5–10 secondi.
La causa è l'utilizzo di .withResponse su una caratteristica che supporta solo .writeWithoutResponse, o viceversa. Verificare characteristic.properties prima di chiamare. Un altro possibile problema è l'MTU: se i dati superano 20 byte (MTU BLE 4.0), è necessaria la negoziazione dell'MTU tramite negotiateMTU o la frammentazione. Utilizzare peripheral.maximumWriteValueLength(for: .withResponse) per determinare la dimensione massima del pacchetto.
CBPeripheral fuori portata non si disconnette immediatamente — iOS lo porta allo stato .disconnected dopo un timeout (solitamente 20–30 secondi). Per il monitoraggio, utilizzare readRSSI su CBPeripheral — se non disponibile, restituirà un errore con codice CBError.connectionTimeout. Monitorare anche centralManager:didDisconnectPeripheral:error: per il rilevamento tempestivo della perdita di connessione.
Core Bluetooth non è thread-safe — tutte le chiamate a CBPeripheral devono essere effettuate dalla stessa coda (solitamente la coda principale o una coda seriale specificata durante l'inizializzazione di CBCentralManager). Chiamate concorrenti da thread diversi causano race condition e crash dell'applicazione. Utilizzare DispatchQueue(label: “com.app.ble”) per tutte le operazioni BLE e DispatchQueue.main.async per gli aggiornamenti dell'interfaccia utente.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche