Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dpearson2699 avatar

Core Bluetooth

  • 2.6k installs
  • 944 repo stars
  • Updated July 15, 2026
  • dpearson2699/swift-ios-skills

core-bluetooth is a Swift iOS skill for BLE central and peripheral GATT communication with Core Bluetooth managers.

About

Core Bluetooth covers scanning, connecting, and exchanging data over Bluetooth Low Energy in Swift 6.3 on iOS 26+. Central role workflows use CBCentralManager to scan for service UUIDs, connect to CBPeripheral devices, discover services and characteristics, and read, write, or subscribe to notifications. Peripheral role workflows publish local GATT services with CBPeripheralManager advertising. Setup requires NSBluetoothAlwaysUsageDescription and optional UIBackgroundModes for bluetooth-central or bluetooth-peripheral. Authorization has no explicit prompt API; apps check manager.authorization and wait for poweredOn before scanning or advertising. Background BLE and state restoration patterns preserve connections across app suspends. Write flow control, CBUUID workflows, and common mistakes like scanning with nil service filters in production are documented. Scope directs privacy-preserving accessory setup to accessorysetupkit first, returning here for post-setup GATT communication. Review checklists verify Info.plist keys, authorization handling, and background mode entitlements before App Review.

  • Central role scan, connect, discover, read, write, and notify patterns.
  • Peripheral role advertising and local GATT service publishing.
  • NSBluetoothAlwaysUsageDescription and background mode setup.
  • Authorization via manager.authorization without explicit permission API.
  • State restoration and background BLE connection preservation.

Core Bluetooth by the numbers

  • 2,646 all-time installs (skills.sh)
  • +117 installs in the week ending Jul 29, 2026 (Skillselion tracking)
  • Ranked #72 of 1,039 Mobile Development skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Jul 31, 2026 (Skillselion catalog sync)
At a glance

core-bluetooth capabilities & compatibility

Capabilities
central manager scanning and connection lifecycl · service and characteristic discovery with read w · peripheral manager advertising and publishing · background ble and state restoration patterns · authorization and poweredon state gating
Use cases
api development · frontend
Platforms
macOS
Runs
Runs locally
Pricing
Free
From the docs

What core-bluetooth says it does

Core Bluetooth has no explicit permission request API.
SKILL.md
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-bluetooth

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2.6k
repo stars944
Security audit3 / 3 scanners passed
Last updatedJuly 15, 2026
Repositorydpearson2699/swift-ios-skills

How do I scan, connect, and exchange BLE characteristic data with correct authorization and background modes?

Implement BLE central and peripheral GATT workflows with CBCentralManager, CBPeripheral, and background modes in Swift.

Who is it for?

iOS developers implementing heart rate monitors, sensors, or custom BLE peripherals in Swift.

Skip if: Skip for initial privacy-preserving accessory picker flows owned by accessorysetupkit.

When should I use this skill?

User implements CBCentralManager scanning, CBPeripheral characteristics, or BLE background modes.

What you get

Working central or peripheral BLE flows with poweredOn gating, GATT discovery, and background restoration.

  • CBCentralManager implementation
  • Characteristic subscription handlers
  • Heart-rate BPM parser

By the numbers

  • Reference eval uses GATT service UUID 180D and characteristic 2A37
  • Skill includes eval-driven safety checks for scan, connect, subscribe, and write flows

Files

SKILL.mdMarkdownGitHub ↗

Core Bluetooth

Scan for, connect to, and exchange data with Bluetooth Low Energy (BLE) devices. Covers the central role (scanning and connecting to peripherals), the peripheral role (advertising services), background modes, and state restoration. Targets Swift 6.3 / iOS 26+. Use accessorysetupkit for privacy-preserving accessory discovery and setup; use this skill for direct Core Bluetooth GATT communication.

Contents

Setup

Info.plist Keys

KeyPurpose
NSBluetoothAlwaysUsageDescriptionRequired. Explains why the app uses Bluetooth
UIBackgroundModes with bluetooth-centralBackground scanning and connecting
UIBackgroundModes with bluetooth-peripheralBackground advertising

Bluetooth Authorization

Core Bluetooth has no explicit permission request API. Add NSBluetoothAlwaysUsageDescription, create the manager when the app is ready for Bluetooth access, then check manager.authorization and manager.state. Treat .denied and .restricted as terminal until the user changes Settings; wait for .poweredOn before scanning, connecting, advertising, or publishing services.

Central Role: Scanning

Creating the Central Manager

Always wait for the poweredOn state before scanning.

import CoreBluetooth

final class BluetoothManager: NSObject, CBCentralManagerDelegate {
    private var centralManager: CBCentralManager!
    private var discoveredPeripheral: CBPeripheral?

    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        guard central.state == .poweredOn else { return }
        startScanning()
    }
}

Scanning for Peripherals

Scan for specific service UUIDs to save power. Pass nil to discover all peripherals (not recommended in production).

let heartRateServiceUUID = CBUUID(string: "180D")

func startScanning() {
    centralManager.scanForPeripherals(
        withServices: [heartRateServiceUUID],
        options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
    )
}

func centralManager(
    _ central: CBCentralManager,
    didDiscover peripheral: CBPeripheral,
    advertisementData: [String: Any],
    rssi RSSI: NSNumber
) {
    guard RSSI.intValue > -70 else { return } // Filter weak signals

    // IMPORTANT: Retain the peripheral -- it will be deallocated otherwise
    discoveredPeripheral = peripheral
    centralManager.stopScan()
    centralManager.connect(peripheral, options: nil)
}

Central Role: Connecting

func centralManager(
    _ central: CBCentralManager,
    didConnect peripheral: CBPeripheral
) {
    peripheral.delegate = self
    peripheral.discoverServices([heartRateServiceUUID])
}

func centralManager(
    _ central: CBCentralManager,
    didDisconnectPeripheral peripheral: CBPeripheral,
    timestamp: CFAbsoluteTime,
    isReconnecting: Bool,
    error: Error?
) {
    if isReconnecting {
        // System is automatically reconnecting
        return
    }
    // Handle disconnection -- optionally reconnect
    discoveredPeripheral = nil
}

Discovering Services and Characteristics

Implement CBPeripheralDelegate to walk the service/characteristic tree.

extension BluetoothManager: CBPeripheralDelegate {
    func peripheral(
        _ peripheral: CBPeripheral,
        didDiscoverServices error: Error?
    ) {
        guard let services = peripheral.services else { return }
        for service in services {
            peripheral.discoverCharacteristics(nil, for: service)
        }
    }

    func peripheral(
        _ peripheral: CBPeripheral,
        didDiscoverCharacteristicsFor service: CBService,
        error: Error?
    ) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
            if characteristic.properties.contains(.notify) {
                peripheral.setNotifyValue(true, for: characteristic)
            }
            if characteristic.properties.contains(.read) {
                peripheral.readValue(for: characteristic)
            }
        }
    }
}

Reading, Writing, and Notifications

Reading a Value

func peripheral(
    _ peripheral: CBPeripheral,
    didUpdateValueFor characteristic: CBCharacteristic,
    error: Error?
) {
    guard let data = characteristic.value else { return }

    switch characteristic.uuid {
    case CBUUID(string: "2A37"):
        if let heartRate = parseHeartRate(data) {
            print("Heart rate: \(heartRate) bpm")
        }
    case CBUUID(string: "2A19"):
        let batteryLevel = data.first.map { Int($0) } ?? 0
        print("Battery: \(batteryLevel)%")
    default:
        break
    }
}

private func parseHeartRate(_ data: Data) -> Int? {
    guard data.count >= 2 else { return nil }
    let flags = data[0]
    let is16Bit = (flags & 0x01) != 0
    if is16Bit {
        guard data.count >= 3 else { return nil }
        return Int(data[1]) | (Int(data[2]) << 8)
    } else {
        return Int(data[1])
    }
}

Writing a Value

func writeValue(_ data: Data, to characteristic: CBCharacteristic,
                on peripheral: CBPeripheral,
                preferResponse: Bool = true) {
    let type: CBCharacteristicWriteType
    if preferResponse, characteristic.properties.contains(.write) {
        type = .withResponse
    } else if characteristic.properties.contains(.writeWithoutResponse),
              peripheral.canSendWriteWithoutResponse {
        type = .withoutResponse
    } else if characteristic.properties.contains(.write) {
        type = .withResponse
    } else {
        return
    }

    guard data.count <= peripheral.maximumWriteValueLength(for: type) else { return }
    peripheral.writeValue(data, for: characteristic, type: type)
}

// Confirmation callback for .withResponse writes.
func peripheral(
    _ peripheral: CBPeripheral,
    didWriteValueFor characteristic: CBCharacteristic,
    error: Error?
) {
    if let error {
        print("Write failed: \(error.localizedDescription)")
    }
}

// Resume queued .withoutResponse writes here.
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {}

Subscribing to Notifications

// Subscribe
peripheral.setNotifyValue(true, for: characteristic)

// Unsubscribe
peripheral.setNotifyValue(false, for: characteristic)

// Confirmation
func peripheral(
    _ peripheral: CBPeripheral,
    didUpdateNotificationStateFor characteristic: CBCharacteristic,
    error: Error?
) {
    if characteristic.isNotifying {
        print("Now receiving notifications for \(characteristic.uuid)")
    }
}

Peripheral Role: Advertising

Publish services from the local device using CBPeripheralManager.

final class BLEPeripheralManager: NSObject, CBPeripheralManagerDelegate {
    private var peripheralManager: CBPeripheralManager!
    private let serviceUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABC")
    private let charUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABD")

    override init() {
        super.init()
        peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
    }

    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        guard peripheral.state == .poweredOn else { return }
        setupService()
    }

    private func setupService() {
        let characteristic = CBMutableCharacteristic(
            type: charUUID,
            properties: [.read, .notify],
            value: nil,
            permissions: [.readable]
        )

        let service = CBMutableService(type: serviceUUID, primary: true)
        service.characteristics = [characteristic]
        peripheralManager.add(service)
    }

    func peripheralManager(
        _ peripheral: CBPeripheralManager,
        didAdd service: CBService,
        error: Error?
    ) {
        guard error == nil else { return }
        peripheralManager.startAdvertising([
            CBAdvertisementDataServiceUUIDsKey: [serviceUUID],
            CBAdvertisementDataLocalNameKey: "MyDevice"
        ])
    }
}

Background BLE

Background Central Mode

Add bluetooth-central to UIBackgroundModes. In the background:

  • Scanning must specify one or more service UUIDs; nil scans are foreground-only
  • Scan options, including CBCentralManagerScanOptionAllowDuplicatesKey, have no effect

Background Peripheral Mode

Add bluetooth-peripheral to UIBackgroundModes. In the background:

  • Without this mode, published service contents are disabled while suspended
  • The local name is not advertised
  • Service UUIDs move to the overflow area and require explicit service scans

State Restoration

State restoration allows the system to re-create your central or peripheral manager after your app is terminated and relaunched for a BLE event.

Central Manager State Restoration

// 1. Create with a restoration identifier
centralManager = CBCentralManager(
    delegate: self,
    queue: nil,
    options: [CBCentralManagerOptionRestoreIdentifierKey: "myCentral"]
)

// 2. Implement the restoration delegate method
func centralManager(
    _ central: CBCentralManager,
    willRestoreState dict: [String: Any]
) {
    if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey]
        as? [CBPeripheral] {
        for peripheral in peripherals {
            // Re-assign delegate and retain
            peripheral.delegate = self
            discoveredPeripheral = peripheral
        }
    }
    let restoredServices = dict[CBCentralManagerRestoredStateScanServicesKey]
        as? [CBUUID]
    let restoredOptions = dict[CBCentralManagerRestoredStateScanOptionsKey]
        as? [String: Any]
    // Resume scanning with restoredServices/restoredOptions if still needed.
}

Peripheral Manager State Restoration

peripheralManager = CBPeripheralManager(
    delegate: self,
    queue: nil,
    options: [CBPeripheralManagerOptionRestoreIdentifierKey: "myPeripheral"]
)

func peripheralManager(
    _ peripheral: CBPeripheralManager,
    willRestoreState dict: [String: Any]
) {
    let services = dict[CBPeripheralManagerRestoredStateServicesKey]
        as? [CBMutableService]
    let advertisement = dict[CBPeripheralManagerRestoredStateAdvertisementDataKey]
        as? [String: Any]
    // Reconnect app state to restored services/advertisement as needed.
}

Common Mistakes

DON'T: Scan or connect before poweredOn

// WRONG: Scanning immediately -- manager may not be ready
let manager = CBCentralManager(delegate: self, queue: nil)
manager.scanForPeripherals(withServices: nil) // May silently fail

// CORRECT: Wait for poweredOn in the delegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
    if central.state == .poweredOn {
        central.scanForPeripherals(withServices: [serviceUUID])
    }
}

DON'T: Lose the peripheral reference

Core Bluetooth does not retain discovered peripherals. If you don't hold a strong reference, the peripheral is deallocated and the connection fails silently.

// WRONG: No strong reference kept
func centralManager(_ central: CBCentralManager,
                    didDiscover peripheral: CBPeripheral, ...) {
    central.connect(peripheral) // peripheral may be deallocated
}

// CORRECT: Retain the peripheral
func centralManager(_ central: CBCentralManager,
                    didDiscover peripheral: CBPeripheral, ...) {
    self.discoveredPeripheral = peripheral // Strong reference
    central.connect(peripheral)
}

DON'T: Scan for nil services in production

// WRONG: Discovers every BLE device in range -- drains battery
centralManager.scanForPeripherals(withServices: nil)

// CORRECT: Specify the service UUIDs you need
centralManager.scanForPeripherals(withServices: [targetServiceUUID])

DON'T: Assume connection order or timing

// WRONG: Assuming immediate connection
centralManager.connect(peripheral)
discoverServicesNow() // Peripheral not connected yet

// CORRECT: Discover services in the didConnect callback
func centralManager(_ central: CBCentralManager,
                    didConnect peripheral: CBPeripheral) {
    peripheral.delegate = self
    peripheral.discoverServices([serviceUUID])
}

DON'T: Write without checking properties and flow control

// WRONG: May fail, report an error, or provide no confirmation
peripheral.writeValue(data, for: characteristic, type: .withResponse)

// CORRECT: Check properties, length, and .withoutResponse flow control
if characteristic.properties.contains(.write),
   data.count <= peripheral.maximumWriteValueLength(for: .withResponse) {
    peripheral.writeValue(data, for: characteristic, type: .withResponse)
} else if characteristic.properties.contains(.writeWithoutResponse),
          peripheral.canSendWriteWithoutResponse,
          data.count <= peripheral.maximumWriteValueLength(for: .withoutResponse) {
    peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
}

Review Checklist

  • [ ] NSBluetoothAlwaysUsageDescription added to Info.plist
  • [ ] All BLE operations gated on centralManagerDidUpdateState returning .poweredOn
  • [ ] Discovered peripherals retained with a strong reference
  • [ ] Scanning uses specific service UUIDs (not nil) in production
  • [ ] CBPeripheralDelegate set before calling discoverServices
  • [ ] Characteristic properties checked before read/write/notify
  • [ ] Write payloads stay within maximumWriteValueLength(for:)
  • [ ] .withoutResponse writes honor canSendWriteWithoutResponse
  • [ ] Background mode (bluetooth-central or bluetooth-peripheral) added if needed
  • [ ] State restoration identifier set if app needs relaunch-on-BLE-event support
  • [ ] willRestoreState delegate method implemented when using state restoration
  • [ ] Scanning stopped after discovering the target peripheral
  • [ ] Disconnection handled with optional automatic reconnect logic
  • [ ] Write type matches characteristic properties (.withResponse vs .withoutResponse)

References

Related skills

How it compares

Pick core-bluetooth for native iOS CBCentralManager and GATT workflows; use cross-platform BLE libraries when the same code must also target Android.

FAQ

Is there a Bluetooth permission prompt API?

No. Add NSBluetoothAlwaysUsageDescription and check manager.authorization and state instead.

Should I scan with nil service UUIDs in production?

No. Scan for specific service UUIDs to save power; nil discovers all peripherals.

When should I use accessorysetupkit instead?

Use accessorysetupkit for privacy-preserving accessory discovery and setup before GATT communication here.

Is Core Bluetooth safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

Mobile Developmentintegrationsfrontend

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.