CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

ble-patterns.mdskills/core-bluetooth/references/

Core Bluetooth Extended Patterns

Overflow reference for the core-bluetooth skill. Contains advanced patterns that exceed the main skill file's scope.

Contents

  • SwiftUI BLE Integration
  • Reconnection Strategies
  • Data Parsing Helpers
  • Multiple Peripheral Management
  • L2CAP Channels
  • Peripheral Role: Responding to Requests

SwiftUI BLE Integration

Observable Bluetooth Manager

import CoreBluetooth
import SwiftUI

@Observable
@MainActor
final class BLEViewModel: NSObject {
    private var centralManager: CBCentralManager!
    private var connectedPeripheral: CBPeripheral?

    var isBluetoothOn = false
    var isScanning = false
    var isConnected = false
    var discoveredDevices: [DiscoveredDevice] = []
    var heartRate: Int = 0

    struct DiscoveredDevice: Identifiable {
        let id: UUID
        let name: String
        let rssi: Int
        let peripheral: CBPeripheral
    }

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

    func startScan() {
        guard isBluetoothOn else { return }
        discoveredDevices.removeAll()
        isScanning = true
        centralManager.scanForPeripherals(
            withServices: [CBUUID(string: "180D")],
            options: nil
        )
    }

    func stopScan() {
        centralManager.stopScan()
        isScanning = false
    }

    func connect(to device: DiscoveredDevice) {
        stopScan()
        connectedPeripheral = device.peripheral
        centralManager.connect(device.peripheral)
    }

    func disconnect() {
        guard let peripheral = connectedPeripheral else { return }
        centralManager.cancelPeripheralConnection(peripheral)
    }
}

extension BLEViewModel: CBCentralManagerDelegate {
    nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
        Task { @MainActor in
            isBluetoothOn = central.state == .poweredOn
        }
    }

    nonisolated func centralManager(
        _ central: CBCentralManager,
        didDiscover peripheral: CBPeripheral,
        advertisementData: [String: Any],
        rssi RSSI: NSNumber
    ) {
        Task { @MainActor in
            let name = peripheral.name ?? "Unknown"
            let device = DiscoveredDevice(
                id: peripheral.identifier,
                name: name,
                rssi: RSSI.intValue,
                peripheral: peripheral
            )
            if !discoveredDevices.contains(where: { $0.id == device.id }) {
                discoveredDevices.append(device)
            }
        }
    }

    nonisolated func centralManager(
        _ central: CBCentralManager,
        didConnect peripheral: CBPeripheral
    ) {
        Task { @MainActor in
            isConnected = true
            peripheral.delegate = self
            peripheral.discoverServices([CBUUID(string: "180D")])
        }
    }

    nonisolated func centralManager(
        _ central: CBCentralManager,
        didDisconnectPeripheral peripheral: CBPeripheral,
        timestamp: CFAbsoluteTime,
        isReconnecting: Bool,
        error: Error?
    ) {
        Task { @MainActor in
            isConnected = false
            connectedPeripheral = nil
        }
    }
}

extension BLEViewModel: CBPeripheralDelegate {
    nonisolated func peripheral(
        _ peripheral: CBPeripheral,
        didDiscoverServices error: Error?
    ) {
        guard let services = peripheral.services else { return }
        for service in services {
            peripheral.discoverCharacteristics(
                [CBUUID(string: "2A37")],
                for: service
            )
        }
    }

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

    nonisolated func peripheral(
        _ peripheral: CBPeripheral,
        didUpdateValueFor characteristic: CBCharacteristic,
        error: Error?
    ) {
        guard let data = characteristic.value, data.count >= 2 else { return }
        let flags = data[0]
        let bpm: Int
        if (flags & 0x01) != 0 {
            bpm = Int(data[1]) | (Int(data[2]) << 8)
        } else {
            bpm = Int(data[1])
        }
        Task { @MainActor in
            heartRate = bpm
        }
    }
}

SwiftUI View

struct HeartRateView: View {
    @State private var viewModel = BLEViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isConnected {
                    VStack {
                        Image(systemName: "heart.fill")
                            .font(.system(size: 60))
                            .foregroundStyle(.red)
                        Text("\(viewModel.heartRate) BPM")
                            .font(.largeTitle.monospacedDigit())
                        Button("Disconnect") { viewModel.disconnect() }
                    }
                } else {
                    List(viewModel.discoveredDevices) { device in
                        Button {
                            viewModel.connect(to: device)
                        } label: {
                            HStack {
                                Text(device.name)
                                Spacer()
                                Text("\(device.rssi) dBm")
                                    .foregroundStyle(.secondary)
                            }
                        }
                    }
                }
            }
            .navigationTitle("Heart Rate Monitor")
            .toolbar {
                if !viewModel.isConnected {
                    Button(viewModel.isScanning ? "Stop" : "Scan") {
                        viewModel.isScanning ? viewModel.stopScan() : viewModel.startScan()
                    }
                }
            }
        }
    }
}

Reconnection Strategies

Auto-Reconnect on Disconnect

func centralManager(
    _ central: CBCentralManager,
    didDisconnectPeripheral peripheral: CBPeripheral,
    timestamp: CFAbsoluteTime,
    isReconnecting: Bool,
    error: Error?
) {
    if !isReconnecting {
        // Attempt to reconnect
        central.connect(peripheral, options: nil)
    }
}

Reconnecting to Known Peripherals

Store the peripheral's UUID and reconnect at next launch using retrievePeripherals(withIdentifiers:).

func reconnectToKnownDevice(uuid: UUID) {
    let peripherals = centralManager.retrievePeripherals(
        withIdentifiers: [uuid]
    )
    if let peripheral = peripherals.first {
        connectedPeripheral = peripheral
        centralManager.connect(peripheral, options: nil)
    }
}

Retrieving Already-Connected Peripherals

If another app has connected to the peripheral, you can retrieve it by service.

func findConnectedHeartRateMonitors() -> [CBPeripheral] {
    centralManager.retrieveConnectedPeripherals(
        withServices: [CBUUID(string: "180D")]
    )
}

Data Parsing Helpers

Generic Data Reader

extension Data {
    func readUInt8(at offset: Int) -> UInt8? {
        guard offset < count else { return nil }
        return self[offset]
    }

    func readUInt16LE(at offset: Int) -> UInt16? {
        guard offset + 1 < count else { return nil }
        return UInt16(self[offset]) | (UInt16(self[offset + 1]) << 8)
    }

    func readUInt32LE(at offset: Int) -> UInt32? {
        guard offset + 3 < count else { return nil }
        return UInt32(self[offset])
            | (UInt32(self[offset + 1]) << 8)
            | (UInt32(self[offset + 2]) << 16)
            | (UInt32(self[offset + 3]) << 24)
    }
}

Battery Level Parser

func parseBatteryLevel(_ data: Data) -> Int? {
    data.readUInt8(at: 0).map { Int($0) }
}

Multiple Peripheral Management

Managing Several Connections

@MainActor
final class MultiDeviceManager: NSObject {
    private var centralManager: CBCentralManager!
    private var connectedPeripherals: [UUID: CBPeripheral] = [:]

    func centralManager(
        _ central: CBCentralManager,
        didDiscover peripheral: CBPeripheral,
        advertisementData: [String: Any],
        rssi RSSI: NSNumber
    ) {
        let id = peripheral.identifier
        guard connectedPeripherals[id] == nil else { return }
        connectedPeripherals[id] = peripheral
        central.connect(peripheral)
    }

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

    func disconnectAll() {
        for peripheral in connectedPeripherals.values {
            centralManager.cancelPeripheralConnection(peripheral)
        }
        connectedPeripherals.removeAll()
    }
}

L2CAP Channels

Use L2CAP channels for higher-throughput, stream-oriented data transfer.

Central Side

func peripheral(
    _ peripheral: CBPeripheral,
    didOpen channel: CBL2CAPChannel?,
    error: Error?
) {
    guard let channel else { return }
    let inputStream = channel.inputStream
    let outputStream = channel.outputStream

    inputStream.delegate = self
    outputStream.delegate = self
    inputStream.schedule(in: .main, forMode: .default)
    outputStream.schedule(in: .main, forMode: .default)
    inputStream.open()
    outputStream.open()
}

// Open a channel to a known PSM
peripheral.openL2CAPChannel(CBL2CAPPSM(0x0025))

Peripheral Side

// Publish a channel listener
peripheralManager.publishL2CAPChannel(withEncryption: true)

func peripheralManager(
    _ peripheral: CBPeripheralManager,
    didPublishL2CAPChannel PSM: CBL2CAPPSM,
    error: Error?
) {
    // Share the PSM with centrals via a characteristic value
    print("Published L2CAP channel on PSM: \(PSM)")
}

Peripheral Role: Responding to Requests

When acting as a peripheral, respond to read and write requests from connected centrals.

extension BLEPeripheralManager: CBPeripheralManagerDelegate {
    func peripheralManager(
        _ peripheral: CBPeripheralManager,
        didReceiveRead request: CBATTRequest
    ) {
        if request.characteristic.uuid == charUUID {
            let value = currentSensorData()
            request.value = value.subdata(
                in: request.offset..<value.count
            )
            peripheral.respond(to: request, withResult: .success)
        } else {
            peripheral.respond(to: request, withResult: .attributeNotFound)
        }
    }

    func peripheralManager(
        _ peripheral: CBPeripheralManager,
        didReceiveWrite requests: [CBATTRequest]
    ) {
        for request in requests {
            if let value = request.value {
                handleIncomingData(value)
            }
        }
        // Respond to the first request -- Core Bluetooth sends the
        // response for all requests in the batch
        if let first = requests.first {
            peripheral.respond(to: first, withResult: .success)
        }
    }

    func peripheralManager(
        _ peripheral: CBPeripheralManager,
        central: CBCentral,
        didSubscribeTo characteristic: CBCharacteristic
    ) {
        // Central subscribed to notifications -- start sending updates
        startSendingUpdates()
    }

    func peripheralManager(
        _ peripheral: CBPeripheralManager,
        central: CBCentral,
        didUnsubscribeFrom characteristic: CBCharacteristic
    ) {
        stopSendingUpdates()
    }
}

skills

core-bluetooth

CHANGELOG.md

README.md

tile.json