Core Bluetooth: Architecture and BLE Development on iOS

Author: IT Sectr Published: 2026-07-16 Reading time: 10 min

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 — Apple’s system framework for BLE development on iOS, iPadOS, and macOS
  • CBCentralManager — class for scanning and connecting to BLE peripherals from the central device side
  • CBPeripheralManager — class for creating a BLE server that publishes services and characteristics
  • GATT Profile — hierarchical model of services, characteristics, and descriptors for data exchange
  • Background Modes — Core Bluetooth supports BLE communication in the background through system delegates and state restoration

What is Core Bluetooth: Architecture and Components

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.

ClassRoleKey Methods
CBCentralManagerCentral devicescanForPeripherals, connect, cancelPeripheralConnection, retrievePeripherals
CBPeripheralRemote peripheraldiscoverServices, discoverCharacteristics, readValue, writeValue, setNotifyValue
CBPeripheralManagerLocal peripheraladdService, removeService, startAdvertising, respondToRequest, updateValue
CBCentralRemote centralmaximumUpdateValueLength, 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: Scanning and Connecting BLE Devices

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).

swift
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: Creating a BLE Server on iOS

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+.

swift
// 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: Read, Write, and 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.

OperationMethodDelegateTransfer Type
ReadreadValueForCharacteristic:didUpdateValueForCharacteristicPolling (request-response)
Write withResponsewriteValue:forCharacteristic:type:withResponsedidWriteValueForCharacteristicWith acknowledgment
Write withoutResponsewriteValue:forCharacteristic:type:withoutResponseNo delegateWithout acknowledgment
NotificationsetNotifyValue:true forCharacteristic:didUpdateNotificationStateForCharacteristic + didUpdateValueForCharacteristicPush from peripheral

Core Bluetooth Background Mode and State Restoration

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.

swift
// 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.

BLE App Example in Swift: Central and Peripheral

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.

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

What is Core Bluetooth?

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.

What is the difference between CBCentralManager and CBPeripheralManager?

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.

How to configure Core Bluetooth for background operation?

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.

Why is Core Bluetooth not finding devices?

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.

Can I connect multiple CBPeripherals simultaneously?

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

  • Core Bluetooth — Apple’s system framework for BLE development with CBCentralManager and CBPeripheralManager classes
  • CBCentralManager manages scanning, connection, and GATT operations with remote BLE devices
  • CBPeripheralManager emulates BLE peripherals with service publishing and incoming request handling
  • GATT Profile includes services, characteristics, and descriptors with read, write, and notification operations
  • Background mode requires UIBackgroundModes and restoreIdentifier for State Restoration
  • MTU limits BLE packet size: 23 bytes for BLE 4.0, up to 251 bytes for BLE 5.0+ with extended MTU
  • Swift async/await via CheckedContinuation simplifies asynchronous BLE code with delegates

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.

Discuss the project

Read also