Core Bluetooth is Apple’s framework for interacting with Bluetooth Low Energy on iOS, iPadOS, and macOS. The framework provides a complete set of APIs for operating in both BLE roles: central device (CBCentralManager) for scanning and connecting to peripherals, and peripheral device (CBPeripheralManager) for emulating a BLE server. Core Bluetooth abstracts the BLE protocol stack from the physical radio to the application-level GATT profile. According to Apple Developer, 2026, Core Bluetooth is the only official Apple API for BLE development, supporting BLE 4.0–5.4 with extended advertising, 2M PHY, and LE Audio.
Key Takeaways
Core Bluetooth divides the BLE stack into two logical roles defined by the Bluetooth SIG specification. The central device role (Central) is represented by the CBCentralManager class — it initiates scanning, establishes connections, and manages the list of connected CBPeripherals. The peripheral device role (Peripheral) is represented by CBPeripheralManager — it publishes services and characteristics, responds to central requests, and sends notifications. A single iOS session can simultaneously operate in both roles on different BLE radios, but a typical application uses one role.
The Core Bluetooth architecture includes five key abstractions. CBCentralManager manages the device’s Bluetooth adapter state: poweredOn (ready to work), poweredOff (Bluetooth disabled), unauthorized (no permission), unsupported (BLE unavailable). CBPeripheral represents a remote BLE device with its UUID, name, RSSI, and GATT hierarchy. CBService — a logical group of characteristics. CBCharacteristic — a data point for read/write/notifications. CBPeripheralManager creates a local GATT server for emulating a peripheral.
| Class | Role | Key Methods |
|---|---|---|
| CBCentralManager | Central device | scanForPeripherals, connect, cancelPeripheralConnection, retrievePeripherals |
| CBPeripheral | Remote peripheral | discoverServices, discoverCharacteristics, readValue, writeValue, setNotifyValue |
| CBPeripheralManager | Local peripheral | addService, removeService, startAdvertising, respondToRequest, updateValue |
| CBCentral | Remote central | maximumUpdateValueLength, identifier, ancsAuthorized |
CBCentralManager States control all BLE operations. When the app starts, centralManagerDidUpdateState is called with the current Bluetooth state. If the state is not .poweredOn, any BLE calls are ignored by the system. The developer must check the state before each scan and connection. Transition from .poweredOff to .poweredOn occurs when Bluetooth is enabled in iOS Settings — the delegate receives a repeated call, and the app can resume scanning.
CBCentralManager is the entry point for all BLE operations on the central device side. Initialization takes a delegate (CBCentralManagerDelegate) and a DispatchQueue — Apple recommends using the main queue for simplicity or a serial queue for performance. After initialization, the framework automatically checks the Bluetooth state and calls centralManagerDidUpdateState: — the first mandatory delegate to handle.
Scanning starts with the scanForPeripheralsWithServices:options: method. The first parameter is an array of CBUUID services for filtering: if the UUIDs of the desired services are known, passing them reduces power consumption and search time. If nil, all BLE devices in range are discovered. Options include .allowDuplicatesKey (repeated discoveries of the same device) and .solicitedServiceUUIDsKey (for services published on the central).
import CoreBluetooth
class BLECentral: NSObject {
private var centralManager: CBCentralManager!
private var discoveredPeripherals: [CBPeripheral] = []
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: .main)
}
// Start BLE scanning
func startScan() {
guard centralManager.state == .poweredOn else {
print("Bluetooth unavailable")
return
}
// Scan all devices (nil = no filter)
centralManager.scanForPeripherals(withServices: nil,
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
}
// Stop scanning
func stopScan() {
centralManager.stopScan()
}
// Connect to selected device
func connect(to peripheral: CBPeripheral) {
centralManager.connect(peripheral, options: nil)
}
}
// MARK: - CBCentralManagerDelegate
extension BLECentral: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
startScan()
}
}
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi: NSNumber) {
if !discoveredPeripherals.contains(where: { $0.identifier == peripheral.identifier }) {
discoveredPeripherals.append(peripheral)
print("Found devices: \(peripheral.name ?? "Unknown"), RSSI: \(rssi)")
}
}
func centralManager(_ central: CBCentralManager,
didConnect peripheral: CBPeripheral) {
print("Connected: \(peripheral.identifier)")
peripheral.delegate = self
peripheral.discoverServices(nil)
}
func centralManager(_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?) {
print("Disconnected: \(peripheral.identifier)")
}
}
The BLECentral class demonstrates the full BLE device scanning and connection cycle. centralManagerDidUpdateState starts scanning when Bluetooth is enabled. didDiscoverPeripheral collects found devices into the discoveredPeripherals array with deduplication by identifier. After connection (didConnect), service discovery starts immediately — this is a mandatory step before any GATT operations.
CBPeripheralManager is the class for emulating a BLE peripheral device on iOS. An app in the peripheral role can publish its services and characteristics, accept incoming read/write requests from a central device, and send notifications. CBPeripheralManager is used for BLE accessories emulated by iPhone: remotes, keyboards, trackers, IoT gateways.
The CBPeripheralManager lifecycle begins with initialization and the CBPeripheralManagerDelegate. After receiving poweredOn confirmation via peripheralManagerDidUpdateState:, services are published (addService:) and advertising starts (startAdvertising:). Advertisement data CBAdvertisementData includes the local name (CBAdvertisementDataLocalNameKey), service UUIDs (CBAdvertisementDataServiceUUIDsKey), and transmit power level (CBAdvertisementDataTxPowerLevelKey). The maximum advertisement packet size is 31 bytes for BLE 4.0, 251 bytes for extended advertising BLE 5.0+.
// BLE peripheral on iOS via CBPeripheralManager
class BLEPeripheral: NSObject {
private var peripheralManager: CBPeripheralManager!
let serviceUUID = CBUUID(string: "1234")
let characteristicUUID = CBUUID(string: "5678")
override init() {
super.init()
peripheralManager = CBPeripheralManager(delegate: self, queue: .main)
}
// Publish service with characteristic
func setupService() {
let characteristic = CBMutableCharacteristic(
type: characteristicUUID,
properties: [.read, .write, .notify],
value: nil,
permissions: [.readable, .writeable]
)
let service = CBMutableService(type: serviceUUID, primary: true)
service.characteristics = [characteristic]
peripheralManager.add(service)
}
// Start advertising
func startAdvertising() {
let advertisementData: [String: Any] = [
CBAdvertisementDataLocalNameKey: "My BLE Device",
CBAdvertisementDataServiceUUIDsKey: [serviceUUID]
]
peripheralManager.startAdvertising(advertisementData)
}
}
// MARK: - CBPeripheralManagerDelegate
extension BLEPeripheral: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
if peripheral.state == .poweredOn {
setupService()
}
}
func peripheralManager(_ peripheral: CBPeripheralManager,
didAdd service: CBService,
error: Error?) {
if error == nil {
startAdvertising()
}
}
// Handle read request
func peripheralManager(_ peripheral: CBPeripheralManager,
didReceiveRead request: CBATTRequest) {
let data = "CurrentValue".data(using: .utf8)!
request.value = data
peripheralManager.respond(to: request, withResult: .success)
}
// Handle write request
func peripheralManager(_ peripheral: CBPeripheralManager,
didReceiveWrite requests: [CBATTRequest]) {
for request in requests {
if let value = request.value {
print("Write: \(value)")
}
}
peripheralManager.respond(to: requests.first!, withResult: .success)
}
}
The BLEPeripheral class creates a BLE server with a single characteristic supporting read, write, and notifications. After initialization, peripheralManagerDidUpdateState publishes the service via addService:, then starts advertising via startAdvertising:. The didReceiveRead and didReceiveWrite handlers respond to incoming GATT requests from the central device. The updateValue:forCharacteristic:onSubscribedCentrals: method is used to send notifications.
GATT operations (Generic Attribute Profile) are the foundation of data exchange in Core Bluetooth. After service and characteristic discovery, the central device can perform three types of operations: reading a characteristic value, writing a value, and subscribing to notifications/indications. Each operation is asynchronous and returns the result through the corresponding CBPeripheralDelegate callback.
Reading is performed by calling readValueForCharacteristic:. The value arrives in peripheral:didUpdateValueForCharacteristic:error:. Important: reading returns the current value from the device, not a cached one. If the device does not support reading (the .read property), the call will return an error. For large values (larger than MTU), BLE automatically fragments and reassembles data at the GATT level.
Writing is performed by calling writeValue:forCharacteristic:type:. BLE supports two write models: withResponse (reliable, with acknowledgment) and withoutResponse (fast, without acknowledgment). The CBCharacteristic.properties property defines the available write types. The maximum size of a single write packet is limited by MTU: 23 bytes for BLE 4.0 (20 bytes of payload + 3 bytes of header), up to 247 bytes for BLE 5.0 with extended MTU (MTU 251).
Notifications are activated by calling setNotifyValue:true forCharacteristic:. After subscription, the peripheral automatically sends updates via peripheral:didUpdateValueForCharacteristic: whenever the characteristic value changes. To disable notifications, call setNotifyValue:false forCharacteristic:. Core Bluetooth automatically manages the CCCD descriptor on the peripheral.
| Operation | Method | Delegate | Transfer Type |
|---|---|---|---|
| Read | readValueForCharacteristic: | didUpdateValueForCharacteristic | Polling (request-response) |
| Write withResponse | writeValue:forCharacteristic:type:withResponse | didWriteValueForCharacteristic | With acknowledgment |
| Write withoutResponse | writeValue:forCharacteristic:type:withoutResponse | No delegate | Without acknowledgment |
| Notification | setNotifyValue:true forCharacteristic: | didUpdateNotificationStateForCharacteristic + didUpdateValueForCharacteristic | Push from peripheral |
Background mode in Core Bluetooth allows BLE apps to continue scanning, maintain connections, and receive notifications while in the background. To activate it, you need to enable the “Uses Bluetooth LE accessories” capability in Xcode (Info.plist → Required background modes → App communicates using Core Bluetooth) and add the “bluetooth-central” key to UIBackgroundModes. For the peripheral role — “bluetooth-peripheral”.
State Restoration is a Core Bluetooth mechanism for restoring the state of BLE connections after an app restart by iOS. When background mode is active and a restoreIdentifier is specified in CBCentralManager or CBPeripheralManager initialization, iOS saves the BLE stack state when the app terminates and restores it on the next launch. The centralManager:willRestoreState: delegate receives a dictionary with saved CBPeripherals and pending connections.
// Core Bluetooth configuration with State Restoration
class BLECentralWithRestoration: NSObject {
let restoreIdentifier = "com.app.blecentral"
private var centralManager: CBCentralManager!
override init() {
super.init()
let options: [String: Any] = [
CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier,
CBCentralManagerOptionShowPowerAlertKey: true
]
centralManager = CBCentralManager(delegate: self,
queue: nil,
options: options)
}
}
extension BLECentralWithRestoration: CBCentralManagerDelegate {
// Restore state after restart
func centralManager(_ central: CBCentralManager,
willRestoreState dict: [String : Any]) {
if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey]
as? [CBPeripheral] {
for peripheral in peripherals {
peripheral.delegate = self
// Restore GATT discovery
peripheral.discoverServices(nil)
}
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Bluetooth ready after restoration")
}
}
}
In the BLECentralWithRestoration configuration, the CBCentralManagerOptionRestoreIdentifierKey key enables state preservation. If the app was terminated by iOS (e.g., due to memory pressure), on the next launch centralManager:willRestoreState: receives a list of previously connected CBPeripherals. The app restores the delegates and performs service rediscovery — the user does not notice the connection interruption. Without State Restoration, all BLE sessions are lost when the app terminates.
A complete example of a BLE app in Swift combines both central and peripheral devices in one project. The app can operate in two modes: discover and connect to BLE devices (Central) or emulate a BLE accessory (Peripheral). Below is an architecture with a common BLE manager that selects the role at startup.
// Universal BLE manager for central and peripheral
class BLEManager {
enum Role {
case central
case peripheral
}
private let role: Role
private var centralManager: CBCentralManager?
private var peripheralManager: CBPeripheralManager?
let advertisedServiceUUID = CBUUID(string: "A001")
init(role: Role) {
self.role = role
switch role {
case .central:
centralManager = CBCentralManager(delegate: nil, queue: .main)
case .peripheral:
peripheralManager = CBPeripheralManager(delegate: nil, queue: .main)
}
}
// Central device: scanning
func scanForDevices() {
centralManager?.scanForPeripherals(withServices: nil, options: nil)
}
// Peripheral device: advertising
func advertiseService() {
let data: [String: Any] = [
CBAdvertisementDataServiceUUIDsKey: [advertisedServiceUUID]
]
peripheralManager?.startAdvertising(data)
}
}
// Usage on startup
let isCentral = UserDefaults.standard.bool(forKey: "isCentral")
let manager = BLEManager(role: isCentral ? .central : .peripheral)
if isCentral {
manager.scanForDevices()
} else {
manager.advertiseService()
}
The BLEManager selects the role at initialization and creates the corresponding Manager (CBCentralManager or CBPeripheralManager). The role flag can be stored in UserDefaults or passed through a configuration server. This approach allows the BLE app to adapt to the use case: at a point of sale, an iPhone works as a central for scanning payment terminals; on an IoT gateway — as a peripheral for collecting data from sensors.
Frequently Asked Questions
Core Bluetooth is Apple’s framework for BLE development on iOS, iPadOS, and macOS. It provides APIs for both central (CBCentralManager) and peripheral (CBPeripheralManager) devices. It supports BLE 4.0–5.4, extended advertising, 2M PHY, and LE Audio. Core Bluetooth is the only official Apple API for BLE communication, required for all iOS apps working with Bluetooth Low Energy.
CBCentralManager is a class for operating in the central device role: it scans for BLE peripherals, establishes connections, reads and writes characteristics. CBPeripheralManager is a class for operating in the peripheral role: it publishes services, responds to read/write requests, and sends notifications. A single iPhone can work in both roles simultaneously through different manager instances.
For background BLE operation, enable the “Uses Bluetooth LE accessories” capability in Xcode and add the “bluetooth-central” key to UIBackgroundModes. For the peripheral role — “bluetooth-peripheral”. Specify a restoreIdentifier when initializing the manager for State Restoration. Without these settings, the app in the background will not receive BLE events and will lose connections.
Common reasons: CBCentralManager.state != .poweredOn (Bluetooth disabled or not authorized), delegate not set, device out of range or not sending advertisement packets. Check the NSBluetoothAlwaysUsageDescription permission in Info.plist, the Bluetooth status in centralManagerDidUpdateState, and make sure scanForPeripherals is called only when .poweredOn.
Yes, Core Bluetooth supports simultaneous connections to multiple BLE devices. Each CBPeripheral is managed independently through its own delegate. iOS limits the number of simultaneous BLE connections at the system level (typically 5–7 for iPhone). For 1:N scenarios (e.g., a fitness center with 10 trackers), queuing and cyclic servicing of peripherals is required.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also