CoreBluetooth для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

CoreBluetooth для Swift: Полное руководство по Bluetooth Low Energy

1. Что это и зачем нужна

CoreBluetooth — это фреймворк от Apple, предоставляющий API для взаимодействия с устройствами по протоколу Bluetooth Low Energy (BLE, Bluetooth 4.0 и выше). Он входит в состав iOS SDK и macOS SDK, что делает его нативным инструментом для разработки под экосистему Apple. В отличие от классического Bluetooth, BLE ориентирован на минимальное энергопотребление, что идеально подходит для носимых устройств, датчиков, умных замков, фитнес-трекеров и IoT-гаджетов.

Зачем нужен CoreBluetooth? Если вы разрабатываете приложение для iPhone, которое должно обмениваться данными с пульсометром, управлять умной лампочкой или получать показания с метеостанции — вам понадобится именно этот фреймворк. Он берёт на себя всю низкоуровневую работу: сканирование эфира, установление соединения, обнаружение сервисов и характеристик, а также чтение/запись данных. CoreBluetooth работает в двух ролях: Central (устройство, которое ищет и подключается) и Peripheral (устройство, которое рекламирует себя и предоставляет данные). В этой статье мы сосредоточимся на роли Central, так как она наиболее востребована в приложениях.

Важно понимать, что CoreBluetooth — это не сторонняя библиотека, а системный фреймворк. Его не нужно устанавливать через менеджер пакетов, он доступен "из коробки" в Xcode. Однако для корректной работы необходимо добавить описание использования Bluetooth в Info.plist (ключ NSBluetoothAlwaysUsageDescription для iOS 13+ или NSBluetoothPeripheralUsageDescription для более старых версий).

2. Установка

CoreBluetooth не требует установки через pip или CocoaPods. Это часть стандартной библиотеки Apple. Для подключения фреймворка к проекту:

  1. Откройте ваш проект в Xcode.
  2. Выберите target вашего приложения.
  3. Перейдите на вкладку "General" → "Frameworks, Libraries, and Embedded Content".
  4. Нажмите "+" и выберите CoreBluetooth.framework.

Либо просто добавьте импорт в код:

import CoreBluetooth

Для работы с Bluetooth на симуляторе iOS 14+ и macOS 11+ это также сработает, но реальные BLE-устройства доступны только на физическом девайсе. На симуляторе можно тестировать только логику сканирования и подключения, но не передачу данных.

3. Быстрый старт — минимальный рабочий пример

Ниже приведён минимальный код, который инициализирует центральное устройство (Central Manager) и начинает сканирование всех доступных BLE-устройств. Результат выводится в консоль.

import UIKit
import CoreBluetooth

class ViewController: UIViewController, CBCentralManagerDelegate {

    var centralManager: CBCentralManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Инициализируем Central Manager с делегатом
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }

    // MARK: - CBCentralManagerDelegate

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            print("Bluetooth включён. Начинаем сканирование...")
            // Начинаем сканирование всех устройств
            centralManager.scanForPeripherals(withServices: nil, options: nil)
        case .poweredOff:
            print("Bluetooth выключен")
        case .unauthorized:
            print("Приложение не авторизовано для Bluetooth")
        case .unknown:
            print("Состояние Bluetooth неизвестно")
        case .unsupported:
            print("Устройство не поддерживает BLE")
        case .resetting:
            print("Bluetooth сбрасывается")
        @unknown default:
            print("Неизвестное состояние")
        }
    }

    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {
        print("Найдено устройство: \(peripheral.name ?? "Без имени"), RSSI: \(RSSI)")
    }
}

Этот пример показывает базовый цикл: инициализация менеджера, проверка состояния Bluetooth и сканирование. Устройства будут выводиться в консоль по мере их обнаружения.

4. Основные методы/функции/классы

Рассмотрим ключевые классы и методы CoreBluetooth. Для каждого приведена сигнатура, описание и рабочий пример.

4.1. CBCentralManager

Сигнатура: init(delegate: CBCentralManagerDelegate?, queue: DispatchQueue?)

Что делает: Инициализирует центральное устройство. Параметр queue позволяет указать очередь для делегатных вызовов (nil — основная очередь).

Пример:

let centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main)

4.2. scanForPeripherals

Сигнатура: func scanForPeripherals(withServices serviceUUIDs: [CBUUID]?, options: [String : Any]? = nil)

Что делает: Начинает сканирование BLE-устройств. Если передать nil в serviceUUIDs, будут найдены все устройства. Для фильтрации по сервисам передайте массив CBUUID.

Пример:

// Сканируем только устройства с сервисом Heart Rate
let heartRateServiceUUID = CBUUID(string: "180D")
centralManager.scanForPeripherals(withServices: [heartRateServiceUUID], options: nil)

4.3. stopScan

Сигнатура: func stopScan()

Что делает: Останавливает текущее сканирование. Экономит батарею и ресурсы.

Пример:

centralManager.stopScan()
print("Сканирование остановлено")

4.4. connect

Сигнатура: func connect(_ peripheral: CBPeripheral, options: [String : Any]? = nil)

Что делает: Устанавливает соединение с найденным периферийным устройством. После успешного подключения вызывается делегатный метод centralManager(_:didConnect:).

Пример:

func centralManager(_ central: CBCentralManager,
                    didDiscover peripheral: CBPeripheral,
                    advertisementData: [String : Any],
                    rssi RSSI: NSNumber) {
    // Сохраняем периферию и подключаемся
    self.discoveredPeripheral = peripheral
    centralManager.connect(peripheral, options: nil)
}

4.5. cancelPeripheralConnection

Сигнатура: func cancelPeripheralConnection(_ peripheral: CBPeripheral)

Что делает: Разрывает соединение с устройством. Вызывается делегатный метод centralManager(_:didDisconnectPeripheral:error:).

Пример:

centralManager.cancelPeripheralConnection(connectedPeripheral)

4.6. CBPeripheral.discoverServices

Сигнатура: func discoverServices(_ serviceUUIDs: [CBUUID]?)

Что делает: Запрашивает список сервисов, доступных на подключённом периферийном устройстве. Результат приходит в делегат peripheral(_:didDiscoverServices:).

Пример:

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    peripheral.delegate = self
    peripheral.discoverServices(nil) // Ищем все сервисы
}

4.7. CBPeripheral.discoverCharacteristics

Сигнатура: func discoverCharacteristics(_ characteristicUUIDs: [CBUUID]?, for service: CBService)

Что делает: После обнаружения сервиса, этот метод позволяет найти характеристики (данные) внутри сервиса. Результат — в peripheral(_:didDiscoverCharacteristicsFor:error:).

Пример:

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    guard let services = peripheral.services else { return }
    for service in services {
        print("Найден сервис: \(service.uuid)")
        peripheral.discoverCharacteristics(nil, for: service)
    }
}

4.8. CBPeripheral.writeValue

Сигнатура: func writeValue(_ data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType)

Что делает: Отправляет данные на периферийное устройство. Тип записи .withResponse гарантирует доставку (вызывается подтверждение), .withoutResponse — быстрее, но без гарантии.

Пример:

// Отправляем команду включения на умную лампочку
let command = Data([0x01]) // 0x01 = включить
peripheral.writeValue(command, for: characteristic, type: .withResponse)

4.9. CBPeripheral.readValue

Сигнатура: func readValue(for characteristic: CBCharacteristic)

Что делает: Читает текущее значение характеристики с устройства. Результат асинхронно возвращается в peripheral(_:didUpdateValueFor:error:).

Пример:

peripheral.readValue(for: batteryCharacteristic)

// В делегате:
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    guard let data = characteristic.value else { return }
    let batteryLevel = data[0]
    print("Уровень заряда батареи: \(batteryLevel)%")
}

4.10. CBPeripheral.setNotifyValue

Сигнатура: func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic)

Что делает: Подписывается на уведомления от характеристики. Когда значение на устройстве меняется, приложение получает обновление через peripheral(_:didUpdateValueFor:error:).

Пример:

// Подписываемся на уведомления от пульсометра
peripheral.setNotifyValue(true, for: heartRateCharacteristic)

5. Реальный практический пример (100-200 строк кода)

Создадим простое приложение, которое подключается к эмулятору BLE-термометра (или реальному устройству с сервисом Health Thermometer, UUID: 1809). Приложение будет сканировать, подключаться, читать температуру и подписываться на её обновления.

import UIKit
import CoreBluetooth

class ThermometerViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {

    // MARK: - Properties
    private var centralManager: CBCentralManager!
    private var discoveredPeripheral: CBPeripheral?
    private var temperatureCharacteristic: CBCharacteristic?

    private let thermometerServiceUUID = CBUUID(string: "1809")
    private let temperatureCharacteristicUUID = CBUUID(string: "2A1C")

    // MARK: - Outlets
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var connectButton: UIButton!

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        centralManager = CBCentralManager(delegate: self, queue: nil)
        temperatureLabel.text = "Поиск..."
    }

    // MARK: - Actions
    @IBAction func connectButtonTapped(_ sender: UIButton) {
        if let peripheral = discoveredPeripheral {
            centralManager.connect(peripheral, options: nil)
            connectButton.isEnabled = false
            connectButton.setTitle("Подключение...", for: .normal)
        } else {
            centralManager.scanForPeripherals(withServices: [thermometerServiceUUID], options: nil)
            connectButton.setTitle("Сканирование...", for: .normal)
        }
    }

    // MARK: - CBCentralManagerDelegate
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            print("Bluetooth готов")
        } else {
            print("Bluetooth недоступен")
        }
    }

    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {
        print("Найден термометр: \(peripheral.name ?? "Неизвестно")")
        discoveredPeripheral = peripheral
        centralManager.stopScan()
        connectButton.setTitle("Подключиться к \(peripheral.name ?? "устройству")", for: .normal)
        connectButton.isEnabled = true
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("Подключено к \(peripheral.name ?? "устройству")")
        peripheral.delegate = self
        peripheral.discoverServices([thermometerServiceUUID])
        connectButton.setTitle("Подключено", for: .normal)
        temperatureLabel.text = "Получение данных..."
    }

    func centralManager(_ central: CBCentralManager,
                        didDisconnectPeripheral peripheral: CBPeripheral,
                        error: Error?) {
        print("Отключено")
        connectButton.setTitle("Подключиться", for: .normal)
        connectButton.isEnabled = true
        temperatureLabel.text = "Отключено"
    }

    // MARK: - CBPeripheralDelegate
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }
        for service in services {
            if service.uuid == thermometerServiceUUID {
                peripheral.discoverCharacteristics([temperatureCharacteristicUUID], for: service)
            }
        }
    }

    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverCharacteristicsFor service: CBService,
                    error: Error?) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
            if characteristic.uuid == temperatureCharacteristicUUID {
                temperatureCharacteristic = characteristic
                // Читаем значение один раз
                peripheral.readValue(for: characteristic)
                // Подписываемся на уведомления
                peripheral.setNotifyValue(true, for: characteristic)
            }
        }
    }

    func peripheral(_ peripheral: CBPeripheral,
                    didUpdateValueFor characteristic: CBCharacteristic,
                    error: Error?)                    

Рекомендаций