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. Для подключения фреймворка к проекту:
- Откройте ваш проект в Xcode.
- Выберите target вашего приложения.
- Перейдите на вкладку "General" → "Frameworks, Libraries, and Embedded Content".
- Нажмите "+" и выберите
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?)