Bluejay

public class Bluejay: NSObject

Bluejay is a simple wrapper around CoreBluetooth that focuses on making a common usage case as straight forward as possible: a single connected peripheral that the user is interacting with regularly (think most personal electronics devices that have an associated iOS app: fitness trackers, guitar amps, etc).

It also supports a few other niceties for simplifying usage, including automatic discovery of services and characteristics as they are used, as well as supporting a background task mode where the interaction with the device can be written as synchronous calls running on a background thread to avoid callback pyramids of death, or heavily chained promises.

  • Helps distinguish one Bluejay instance from another.

    Declaration

    Swift

    public var uuid = UUID()
  • Allows checking whether Bluetooth is powered on.

    Declaration

    Swift

    public var isBluetoothAvailable: Bool
  • Allows checking whether Bluejay is currently connecting to a peripheral.

    Declaration

    Swift

    public var isConnecting: Bool
  • Allows checking whether Bluejay is currently connected to a peripheral.

    Declaration

    Swift

    public var isConnected: Bool
  • Allows checking whether Bluejay is currently disconnecting from a peripheral.

    Declaration

    Swift

    public var isDisconnecting: Bool = false
  • Allows checking whether Bluejay is currently scanning.

    Declaration

    Swift

    public var isScanning: Bool
  • Initializing a Bluejay instance will not yet initialize the CoreBluetooth stack. An explicit call to start running a Bluejay instance after it is intialized is required because in cases where a state resotration is trying to restore a listen on a characteristic, a listen restorer must be available before the CoreBluetooth stack is re-initialized. This two-step startup allows you to insert and gaurantee the setup of your listen restorer in between the initialization of Bluejay and the initialization of the CoreBluetooth stack.

    Declaration

    Swift

    public override init()
  • Starting Bluejay will initialize the CoreBluetooth stack. Initializing a Bluejay instance will not yet initialize the CoreBluetooth stack. An explicit call to start running a Bluejay instance after it is intialized is required because in cases where a state resotration is trying to restore a listen on a characteristic, a listen restorer must be available before the CoreBluetooth stack is re-initialized. This two-step startup allows you to insert and gaurantee the setup of your listen restorer in between the initialization of Bluejay and the initialization of the CoreBluetooth stack.

    Declaration

    Swift

    public func start(
        connectionObserver observer: ConnectionObserver? = nil,
        backgroundRestore restoreMode: BackgroundRestoreMode = .disable
        )

    Parameters

    observer

    An object interested in observing Bluetooth connection events and state changes. You can register more observers using the register function.

    restoreMode

    Determines whether Bluejay will opt-in to state restoration, and if so, can optionally provide a listen restorer as well for restoring listens.

  • This will cancel the current and all pending operations in the Bluejay queue, as well as stop any ongoing scan, and disconnect any connected peripheral.

    Declaration

    Swift

    public func cancelEverything(_ error: Error? = nil)

    Parameters

    error

    If nil, all tasks in the queue will be cancelled without any errors. If an error is provided, all tasks in the queue will be failed with the supplied error.

  • This will remove any cached listens associated with the receiving Bluejay’s restore identifier. Call this if you want to stop Bluejay from attempting to restore any listens when state restoration occurs.

    Note

    For handling a single specific characteristic, use endListen. If that succeeds, it will not only stop the listening on that characteristic, it will also remove that listen from the cache for state restoration if listen restoration is enabled, and if that listen was indeed cached for restoration.

    Declaration

    Swift

    public func clearListenCaches()
  • Register for notifications on Bluetooth connection events and state changes. Unregistering is not required, Bluejay will unregister for you if the observer is no longer in memory.

    Declaration

    Swift

    public func register(observer: ConnectionObserver)

    Parameters

    observer

  • Unregister for notifications on Bluetooth connection events and state changes. Unregistering is not required, Bluejay will unregister for you if the observer is no longer in memory.

    Declaration

    Swift

    public func unregister(observer: ConnectionObserver)

    Parameters

    observer

  • Scan for the peripheral(s) specified.

    Declaration

    Swift

    public func scan(
        duration: TimeInterval = 0,
        allowDuplicates: Bool = false,
        serviceIdentifiers: [ServiceIdentifier]?,
        discovery: @escaping (ScanDiscovery, [ScanDiscovery]) -> ScanAction,
        expired: ((ScanDiscovery, [ScanDiscovery]) -> ScanAction)? = nil,
        stopped: @escaping ([ScanDiscovery], Error?) -> Void
        )

    Parameters

    duration

    Stops the scan when the duration in seconds is reached. Defaults to zero (indefinite).

    allowDuplicates

    Determines whether a previously scanned peripheral is allowed to be discovered again.

    serviceIdentifiers

    Specifies what visible services the peripherals must have in order to be discovered.

    discovery

    Called whenever a specified peripheral has been discovered.

    expired

    Called whenever a previously discovered peripheral has not been seen again for a while, and Bluejay is predicting that it may no longer be in range. (Only for a scan with allowDuplicates enabled)

    stopped

    Called when the scan is finished and provides an error if there is any.

  • Stops an ongoing scan if there is one, otherwise it does nothing.

    Declaration

    Swift

    public func stopScanning()
  • Attempt to connect directly to a known peripheral. The call will fail if Bluetooth is not available, or if Bluejay is already connected. Making a connection request while Bluejay is scanning will also cause Bluejay to stop the current scan for you behind the scene prior to fulfilling your connection request.

    Declaration

    Swift

    public func connect(_ peripheralIdentifier: PeripheralIdentifier, timeout: ConnectionTimeout, completion: @escaping (ConnectionResult) -> Void)

    Parameters

    peripheralIdentifier

    The peripheral to connect to.

    timeout

    Specify how long the connection time out should be.

    completion

    Called when the connection request has fully finished and indicates whether it was successful, cancelled, or failed.

  • Disconnect the currently connected peripheral. Providing a completion block is not necessary, but useful in most cases.

    Declaration

    Swift

    public func disconnect(completion: ((DisconnectionResult) -> Void)? = nil)

    Parameters

    completion

    Called when the disconnection request has fully finished and indicates whether it was successful, cancelled, or failed.

  • Read from the specified characteristic.

    Declaration

    Swift

    public func read<R: Receivable>(from characteristicIdentifier: CharacteristicIdentifier, completion: @escaping (ReadResult<R>) -> Void)

    Parameters

    characteristicIdentifier

    The characteristic to read from.

    completion

    Called with the result of the attempt to read from the specified characteristic.

  • Write to the specified characteristic.

    Declaration

    Swift

    public func write<S: Sendable>(to characteristicIdentifier: CharacteristicIdentifier, value: S, type: CBCharacteristicWriteType = .withResponse, completion: @escaping (WriteResult) -> Void)

    Parameters

    characteristicIdentifier

    The characteristic to write to.

    type

    Write type.

    completion

    Called with the result of the attempt to write to the specified characteristic.

  • Listen for notifications on the specified characteristic.

    Declaration

    Swift

    public func listen<R: Receivable>(to characteristicIdentifier: CharacteristicIdentifier, completion: @escaping (ReadResult<R>) -> Void)

    Parameters

    characteristicIdentifier

    The characteristic to listen to.

    completion

    Called with the result of the attempt to listen for notifications on the specified characteristic.

  • End listening on the specified characteristic.

    Declaration

    Swift

    public func endListen(to characteristicIdentifier: CharacteristicIdentifier, completion: ((WriteResult) -> Void)? = nil)

    Parameters

    characteristicIdentifier

    The characteristic to stop listening to.

    completion

    Called with the result of the attempt to stop listening to the specified characteristic.

  • Restore a (believed to be) active listening session, so if we start up in response to a notification, we can receive it.

    Declaration

    Swift

    public func restoreListen<R: Receivable>(to characteristicIdentifier: CharacteristicIdentifier, completion: @escaping (ReadResult<R>) -> Void)

    Parameters

    characteristicIdentifier

    The characteristic that needs the restoration.

    completion

    Called with the result of the attempt to restore the listen on the specified characteristic.

  • One of the three ways to run a background task using a synchronous interface to the Bluetooth peripheral. This is the simplest one as the background task will not return any typed values back to the completion block on finishing the background task, except for thrown errors, and it also doesn’t provide an input for an object that might need thread safe access.

    Warning

    Be careful not to access anything that is not thread safe inside background task.

    Declaration

    Swift

    public func run(
        backgroundTask: @escaping (SynchronizedPeripheral) throws -> Void,
        completionOnMainThread: @escaping (RunResult<Void>) -> Void)

    Parameters

    backgroundTask

    A closure with the jobs to be executed in the background.

    completionOnMainThread

    A closure called on the main thread when the background task has either completed or failed.

  • One of the three ways to run a background task using a synchronous interface to the Bluetooth peripheral. This one allows the background task to potentially return a typed value back to the completion block on finishing the background task successfully.

    Warning

    Be careful not to access anything that is not thread safe inside background task.

    Declaration

    Swift

    public func run<Result>(
        backgroundTask: @escaping (SynchronizedPeripheral) throws -> Result,
        completionOnMainThread: @escaping (RunResult<Result>) -> Void)

    Parameters

    backgroundTask

    A closure with the jobs to be executed in the background.

    completionOnMainThread

    A closure called on the main thread when the background task has either completed or failed.

  • One of the three ways to run a background task using a synchronous interface to the Bluetooth peripheral. This one allows the background task to potentially return a typed value back to the completion block on finishing the background task successfully, as well as supplying an object for thread safe access inside the background task.

    Warning

    Be careful not to access anything that is not thread safe inside background task.

    Declaration

    Swift

    public func run<UserData, Result>(
        userData: UserData,
        backgroundTask: @escaping (SynchronizedPeripheral, UserData) throws -> Result,
        completionOnMainThread: @escaping (RunResult<Result>) -> Void)

    Parameters

    userData

    Any object you wish to have thread safe access inside background task.

    backgroundTask

    A closure with the jobs to be executed in the background.

    completionOnMainThread

    A closure called on the main thread when the background task has either completed or failed.

  • A helper function to take an array of Sendables and combine their data together.

    Declaration

    Swift

    public static func combine(sendables: [Sendable]) -> Data

    Parameters

    sendables

    An array of Sendables whose Data should be appended in the order of the given array.

    Return Value

    The resulting data of all the Sendables combined in the order of the passed in array.

  • Bluejay uses this to figure out whether Bluetooth is available or not.

    • If Bluetooth is available for the first time, start running the queue.
    • If Bluetooth is available for the first time and the app is already connected, then this is a state restoration event. Try listen restoration if possible.
    • If Bluetooth is turned off, cancel everything with the bluetoothUnavailable error and disconnect.
    • Broadcast state changes to observers.

    Declaration

    Swift

    public func centralManagerDidUpdateState(_ central: CBCentralManager)
  • If Core Bluetooth will restore state, update Bluejay’s internal states to match the states of the Core Bluetooth stack by assigning the peripheral to connectingPeripheral or connectedPeripheral, or niling them out, depending on what the restored CBPeripheral state is.

    Declaration

    Swift

    public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any])
  • When connected, update Bluejay’s states by updating the values for connectingPeripheral, connectedPeripheral, and shouldAutoReconnect. Also, make sure to broadcast the event to observers, and notify the queue so that the current operation in-flight can process this event and get a chance to finish.

    Declaration

    Swift

    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
  • Handle a disconnection event from Core Bluetooth by figuring out what kind of disconnection it is (planned or unplanned), and updating Bluejay’s internal state and sending notifications as appropriate.

    Declaration

    Swift

    public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)
  • This mostly happens when either the Bluetooth device or the Core Bluetooth stack somehow only partially completes the negotiation of a connection. For simplicity, Bluejay is currently treating this as a disconnection event, so it can perform all the same clean up logic.

    Declaration

    Swift

    public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?)
  • This should only be called when the current operation in the queue is a Scan task.

    Declaration

    Swift

    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)