swift-async-bluetooth-dependency

0.1.0

Async/await bluetooth dependency for swift-dependencies
agarrharr/swift-async-bluetooth-dependency

What's New

0.1.0

2026-07-15T16:21:24Z

Initial release.

swift-async-bluetooth-dependency

A Bluetooth Low Energy (BLE) client for the swift-dependencies library: an async/await interface over CoreBluetooth that you can control in tests and previews.

@Dependency(\.bluetooth) var bluetooth

Installation

Add the package to your Package.swift:

.package(url: "https://github.com/agarrharr/swift-async-bluetooth-dependency", from: "0.1.0"),

And add the product to any target that needs it:

.product(name: "AsyncBluetooth", package: "swift-async-bluetooth-dependency"),

Remember to add the usual CoreBluetooth entries to your app's Info.plist (NSBluetoothAlwaysUsageDescription).

Usage

A typical scan → connect → discover → read/write/subscribe session:

import AsyncBluetooth
import Dependencies

@Dependency(\.bluetooth) var bluetooth

let heartRateServiceUUID = "180D"
let heartRateCharacteristicUUID = "2A37"

// Scan until the device you want appears. Cancelling the stream stops the scan.
var peripheral: Peripheral?
for await discovered in await bluetooth.scanForPeripherals(serviceUUID: heartRateServiceUUID) {
  if discovered.name == "My Device" {
    peripheral = discovered
    break
  }
}

guard let peripheral else { return }
try await bluetooth.connect(peripheral: peripheral)
try await bluetooth.discoverServices(serviceUUIDs: [heartRateServiceUUID])
try await bluetooth.discoverCharacteristics(
  characteristicUUIDs: [heartRateCharacteristicUUID],
  serviceUUID: heartRateServiceUUID
)

// Read and write.
let value = try await bluetooth.readCharacteristic(
  characteristicUUID: heartRateCharacteristicUUID,
  serviceUUID: heartRateServiceUUID
)
try await bluetooth.writeCharacteristic(
  data: Data([0x01]),
  characteristicUUID: heartRateCharacteristicUUID,
  serviceUUID: heartRateServiceUUID
)

// Subscribe to notifications.
for await value in try await bluetooth.subscribeToCharacteristic(
  characteristicUUID: heartRateCharacteristicUUID,
  serviceUUID: heartRateServiceUUID
) {
  // handle each update
}

// React to the peripheral dropping the connection unexpectedly.
for await _ in await bluetooth.disconnectionEvents() {
  // reconnect, surface an alert, etc.
}

Peripheral is an opaque value type (id + name). CoreBluetooth types never appear in the API, so every endpoint can be exercised in tests without hardware.

Timeouts

CoreBluetooth never times out a connection attempt on its own; a connect to an unresponsive peripheral would suspend forever. The live client applies a timeout (default 10 seconds) to all request/response operations (connect, discovers, read, and write) and throws BluetoothError.timeout when it elapses. Open-ended streams (scanning, subscriptions, disconnection events) never time out.

Customize the policy at your app's entry point:

prepareDependencies {
  $0.bluetooth = .live(
    configuration: BluetoothClient.Configuration(
      connectTimeout: .seconds(30),
      requestTimeout: .seconds(5)
    )
  )
}

Timeouts are driven by @Dependency(\.continuousClock), so they are controllable in tests.

Testing

Every endpoint is a mutable closure, so tests override exactly what they need. With swift-testing and the .dependencies trait:

import AsyncBluetooth
import DependenciesTestSupport
import Testing

@Test(
  .dependencies {
    $0.bluetooth.connect = { _ in }
    $0.bluetooth.readCharacteristic = { _, _ in Data([0x2A]) }
  }
)
func heartRateIsRead() async throws {
  // Exercise code that uses @Dependency(\.bluetooth)...
}

The default testValue is unimplemented: any endpoint your feature calls without an override fails the test, so tests always declare exactly the Bluetooth behavior they depend on. Since Peripheral is a plain value type, scan results are trivial to fake:

$0.bluetooth.scanForPeripherals = { _ in
  AsyncStream {
    $0.yield(Peripheral(id: UUID(0), name: "Test Device"))
    $0.finish()
  }
}

Limitations

  • One connection at a time. The live client manages a single connected peripheral. Multi- peripheral support may come in a future release.
  • String UUIDs. Service and characteristic UUIDs are strings, forwarded to CBUUID(string:). And like CoreBluetooth itself, it crashes on malformed input.
  • Central role only. Acting as a peripheral (advertising) is not supported.

License

This library is released under the MIT license. See LICENSE for details.

Description

  • Swift Tools 6.0.0
View More Packages from this Author

Dependencies

Last updated: Sun Jul 26 2026 11:26:45 GMT-0900 (Hawaii-Aleutian Daylight Time)