CBPeripheral: що це, методи та управління BLE-периферією на iOS

Автор: IT Sectr Опубліковано: 2026-07-16 Час читання: 10 хв

CBPeripheral — клас фреймворку Core Bluetooth, що представляє віддалений BLE-пристрій на iOS. Кожен об'єкт CBPeripheral інкапсулює UUID, ім'я, RSSI та ієрархію GATT-сервісів підключеного BLE-девайса. Розробник взаємодіє з периферією виключно через CBPeripheral: discovery сервісів (discoverServices:), читання характеристик (readValueForCharacteristic:), запис даних (writeValue:forCharacteristic:type:) та підписка на сповіщення (setNotifyValue:forCharacteristic:). За даними Apple Developer, 2026, CBPeripheral — центральний об'єкт для всіх операцій з BLE-периферією, що повертається CBCentralManager при виявленні або підключенні пристрою.

Головне

  • CBPeripheral — клас Core Bluetooth для роботи з віддаленим BLE-пристроєм на iOS
  • Ієрархія GATT — Peripheral містить сервіси (CBService), сервіси містять характеристики (CBCharacteristic), характеристики містять дескриптори (CBDescriptor)
  • Discovery — discoverServices: та discoverCharacteristics:forService: для отримання GATT-структури пристрою
  • Читання та запис — readValueForCharacteristic: та writeValue:forCharacteristic:type: з підтвердженням (withResponse) або без (withoutResponse)
  • Сповіщення — setNotifyValue:forCharacteristic: вмикає підписку на зміни характеристик BLE-пристрою

Що таке CBPeripheral: суть та призначення

CBPeripheral — це об'єкт, що представляє віддалений BLE-пристрій у додатку iOS. На відміну від CBCentralManager, який керує локальним Bluetooth-адаптером iPhone, CBPeripheral моделює зовнішній периферійний пристрій: датчик, фітнес-трекер, маячок, медичний прилад. Кожен екземпляр CBPeripheral містить унікальний ідентифікатор (UUID), який зберігається між сесіями підключення — Apple пов'язує UUID з конкретним пристроєм через системний Bonding.

CBPeripheral не створюється безпосередньо через init. Фреймворк Core Bluetooth повертає об'єкт CBPeripheral у двох сценаріях: при виявленні пристрою через scanForPeripheralsWithServices: (делегат didDiscoverPeripheral) та при підключенні до раніше відомого девайса через retrievePeripheralsWithIdentifiers:. Після отримання об'єкта розробник викликає connectPeripheral: на CBCentralManager, після чого CBPeripheral стає доступним для GATT-операцій.

Життєвий цикл CBPeripheral включає шість станів: disconnected (початковий), connecting (після виклику connect), connected (після didConnectPeripheral), discovering (під час виклику discoverServices), discovered (після отримання сервісів) та disconnecting (після cancelPeripheralConnection). Кожен стан відстежується через делегат CBPeripheralDelegate — must-have протокол для будь-якого BLE-додатку на iOS.

CBPeripheral та ієрархія GATT: сервіси, характеристики, дескриптори

CBPeripheral зберігає ієрархічну GATT-структуру, що складається з трьох рівнів. Кореневий рівень — масив CBService (сервіси), кожен сервіс містить масив CBCharacteristic (характеристики), кожна характеристика містить масив CBDescriptor (дескриптори). Ця модель повністю відповідає специфікації Bluetooth GATT: сервіс — функція пристрою (наприклад, «Heart Rate Service»), характеристика — конкретне значення (пульс 72 bpm), дескриптор — метадані характеристики (одиниці вимірювання, конфігурація сповіщень).

РівеньКлас Core BluetoothОпис
СервісCBServiceЛогічна група споріднених характеристик, ідентифікується UUID (16-bit, 32-bit або 128-bit)
ХарактеристикаCBCharacteristicКонкретне значення даних, підтримує читання, запис, сповіщення
ДескрипторCBDescriptorМетадані характеристики: клієнтська конфігурація CCCD, User Description, Presentation Format

Стандартні BLE-сервіси зареєстровані Bluetooth SIG: Heart Rate Service (UUID 180D), Battery Service (180F), Device Information (180A), Blood Pressure (1810). Для кастомних сервісів використовуються 128-bit UUID (наприклад, E20A39F4-73F5-4BC4-A12F-17D1AD07A961). iOS автоматично розпізнає стандартні UUID та відображає людиночитані назви; кастомні UUID відображаються в hex-форматі.

Після підключення ієрархія CBPeripheral порожня — сервіси та характеристики не завантажені. Розробник зобов'язаний викликати discoverServices: для отримання сервісів і потім для кожного сервісу викликати discoverCharacteristics:forService:. Якщо сервіс містить інклюдовані сервіси, додатково викликається discoverIncludedServices:forService:. Тільки після завершення discovery ієрархії CBPeripheral заповнюється та стає доступним для читання та запису.

Discovery сервісів та характеристик: методи та делегати

Discovery (виявлення) GATT-структури CBPeripheral — обов'язковий крок перед будь-якими операціями читання або запису. Метод discoverServices: запускає асинхронний пошук всіх сервісів пристрою. Якщо передано nil, виявляються всі сервіси; якщо передано масив CBUUID — тільки сервіси із зазначеними UUID (оптимізація часу). Результат приходить у делегат peripheral:didDiscoverServices: — об'єкт CBPeripheral заповнює властивість services масивом CBService.

Після отримання сервісів для кожного CBService необхідно викликати discoverCharacteristics:forService:. Аналогічно, nil — всі характеристики, масив CBUUID — тільки зазначені. Результат: peripheral:didDiscoverCharacteristicsForService:error:. На цьому етапі CBCharacteristic отримує властивості (properties: .read, .write, .notify, .indicate), які визначають дозволені операції.

swift
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)")
    }
}

У прикладі CBPeripheralDelegate реалізовано три обов'язкові методи виявлення. didDiscoverServices перебирає всі знайдені сервіси та запитує характеристики. didDiscoverCharacteristicsForService перевіряє властивості кожної характеристики: для .read викликає readValue, для .notify — setNotifyValue(true). Метод didUpdateValueForCharacteristic отримує актуальне значення у форматі Data.

Читання та запис характеристик: withResponse та withoutResponse

Читання значень CBCharacteristic виконується методом readValueForCharacteristic:. Результат асинхронно приходить у peripheral:didUpdateValueForCharacteristic:error:. Важливо: на пристрої може бути кешоване значення (characteristic.value доступний відразу після discovery), але для отримання актуального виклик readValue обов'язковий. iOS може кешувати значення для енергоефективності — readValue оновлює кеш.

Запис значень виконується методом writeValue:forCharacteristic:type:. Параметр type визначає тип запису: .withResponse (CBCharacteristicWriteWithResponse) — пристрій підтверджує запис через didWriteValueForCharacteristic; .withoutResponse (CBCharacteristicWriteWithoutResponse) — запис без підтвердження, максимальна швидкість, але без гарантії доставки. Специфікація BLE обмежує MTU (Maximum Transmission Unit): до 23 bytes для BLE 4.0, до 251 bytes для BLE 5.0+. Для даних більше MTU потрібна фрагментація на рівні додатку.

swift
// 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 })
    }
}

Вибір типу запису withResponse або withoutResponse залежить від вимоги до надійності. Для команд (увімкнути світло, відкрити замок) використовуйте withResponse — гарантія доставки критична. Для потокових даних (пульс, температура) використовуйте withoutResponse — втрата одного пакета несуттєва. BLE-пристрій може підтримувати лише один тип запису — перевіряйте властивість characteristic.properties.contains(.write) та .writeWithoutResponse.

Підписка на сповіщення BLE через setNotifyValue

Сповіщення (notifications) — механізм BLE, при якому периферійний пристрій надсилає значення характеристики центральному пристрою асинхронно, без постійного polling з боку центрального. CBPeripheral вмикає підписку через метод setNotifyValue:forCharacteristic:. Після активації підписки iOS автоматично записує CCCD (Client Characteristic Configuration Descriptor) на периферії, і пристрій починає надсилати оновлення при кожній зміні значення.

На відміну від індикацій (indicate), сповіщення не потребують підтвердження від центрального — пакет надіслано і забуто. Це дає максимальну пропускну здатність, але можлива втрата пакетів. Індикації потребують підтвердження на рівні протоколу (L2CAP) — надійніше, але повільніше. CBCharacteristic через властивість properties точно вказує, який режим підтримує: .notify, .indicate або обидва.

При відключенні CBPeripheral (disconnect, вихід із зони дії) всі активні підписки автоматично скидаються. При повторному підключенні необхідно заново викликати setNotifyValue:true для кожної характеристики. iOS також втрачає підписки при виході додатку з foreground (якщо не включено background mode) — для фонової роботи потрібно включити capability «Uses Bluetooth LE accessories» в Info.plist.

swift
// 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)")
        }
    }
}

Менеджер підписок NotificationManager демонструє коректну роботу зі сповіщеннями CBPeripheral. subscribeToAllNotifications перебирає всі сервіси та характеристики, активуючи .notify та .indicate. subscribedCharacteristics відстежує активні підписки для коректної відписки. didUpdateNotificationStateForCharacteristic підтверджує успішну зміну стану підписки через властивість characteristic.isNotifying.

Повний приклад роботи з CBPeripheral на Swift

Повний цикл роботи з CBPeripheral включає: отримання об'єкта від CBCentralManager, підключення, discovery, читання/запис, підписку на сповіщення та відключення. У прикладі нижче реалізовано клас BLEConnection, який керує повним життєвим циклом BLE-периферії на Swift з використанням сучасного async/await API (iOS 15+).

swift
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
}

Клас BLEConnection використовує Swift Concurrency (async/await) через CheckedContinuation — сучасний патерн для роботи з делегатними API Core Bluetooth. connect(to:) очікує підтвердження підключення через didConnectPeripheral, discoverServices() — через didDiscoverServices. Такий підхід позбавляє від вкладених делегатів і робить BLE-код лінійним та читабельним. Error handling через BLEError покриває всі типові сценарії відмови BLE-з'єднання.

Часті запитання

Як отримати CBPeripheral без сканування?

CBPeripheral для раніше підключеного пристрою можна отримати через retrievePeripheralsWithIdentifiers: на CBCentralManager. Передайте масив UUID (NSUUID) раніше збережених пристроїв — фреймворк поверне масив CBPeripheral для девайсів у системній базі BLE-бондингу. Це працює тільки для пристроїв, з якими iPhone був раніше спряжений. Для нового пристрою сканування обов'язкове.

Чому CBPeripheral не виявляє сервіси?

Основні причини: пристрій знаходиться поза зоною дії (RSSI нижче порогу), BLE-радіо вимкнено (CBCentralManager.state != .poweredOn), делегат CBPeripheralDelegate не встановлено (peripheral.delegate = self), або виклик discoverServices виконано до підключення. Перевірте статус centralManager.state, переконайтеся, що делегат встановлено до виклику connect та використовуйте retry з таймаутом 5–10 секунд.

Що робити, якщо writeValue не відповідає?

Причина — використання .withResponse на характеристиці, що підтримує тільки .writeWithoutResponse, або навпаки. Перевірте characteristic.properties перед викликом. Також можлива проблема MTU: якщо дані > 20 bytes (BLE 4.0 MTU), необхідне узгодження MTU через negotiateMTU або фрагментація. Використовуйте peripheral.maximumWriteValueLength(for: .withResponse) для визначення максимального розміру пакета.

Як відрізнити CBPeripheral в зоні дії від недоступного?

CBPeripheral поза зоною дії не відключається миттєво — iOS переводить його в стан .disconnected через таймаут (зазвичай 20–30 секунд). Для моніторингу використовуйте readRSSI на CBPeripheral — при недоступності поверне помилку з кодом CBError.connectionTimeout. Також відстежуйте centralManager:didDisconnectPeripheral:error: для своєчасного виявлення розриву з'єднання.

Чи можна використовувати один CBPeripheral з кількох потоків?

Core Bluetooth не потокобезпечний — всі виклики CBPeripheral повинні виконуватися з однієї черги (зазвичай main queue або послідовна serial queue, вказана при ініціалізації CBCentralManager). Одночасні виклики з різних потоків призводять до race condition та падіння додатку. Використовуйте DispatchQueue(label: «com.app.ble») для всіх BLE-операцій та DispatchQueue.main.async для оновлення UI.

Підсумки

  • CBPeripheral — клас Core Bluetooth для роботи з віддаленим BLE-пристроєм на iOS, що повертається CBCentralManager
  • GATT-ієрархія складається з сервісів (CBService), характеристик (CBCharacteristic) та дескрипторів (CBDescriptor) з 16-bit або 128-bit UUID
  • Discovery виконується послідовно: discoverServices: → discoverCharacteristics:forService: з обробкою через делегат
  • Читання — readValueForCharacteristic:, запис — writeValue:forCharacteristic:type: (.withResponse або .withoutResponse)
  • Сповіщення — setNotifyValue:forCharacteristic: активує асинхронне надсилання даних з периферії на централь
  • MTU BLE 4.0 обмежує пакет 23 байтами, BLE 5.0+ — до 251 байта, дані більше MTU потребують фрагментації
  • Async/await Swift через CheckedContinuation спрощує BLE-код, замінюючи вкладені делегати на лінійні виклики

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також