异步核心蓝牙 (Async Core Bluetooth)

Build and Test

这个库是对 CoreBluetoothCoreBluetoothMock 的 Swift 6 封装,它允许你使用 Swift 并发来编写你的核心蓝牙代码。它也能很好地与 SwiftUI 配合使用。

主要类是:

如果您了解 CoreBluetooth API,您应该能够毫无问题地使用此库。

局限性

这个库只支持中心角色,不支持外围角色。这意味着您只能使用这个库来扫描和连接到外围设备,而不能使用它来广播或充当外围设备。该功能可能会在未来添加,但由于将苹果设备用作外围设备不是很常见,因此优先级不高。

它也处于开发的早期阶段,所以以下是一些尚未实现的功能:

所以最基本的功能可以正常工作,但其余的功能还在开发中。

核心蓝牙 (Core Bluetooth)

Swift Package Manager

将 AsyncCoreBluetooth 作为依赖项添加到您的 Package.swift 文件中。

dependencies: [
    .package(url: "https://github.com/meech-ward/AsyncCoreBluetooth.git", from: "0.1.0")
]

然后将其添加到您的目标依赖项中。

targets: [
    .target(
        name: "YourTarget",
        dependencies: ["AsyncCoreBluetooth"]),
]

示例

查看示例 iOS 应用程序以获取完整示例:https://github.com/meech-ward/AsyncCoreBluetoothExample

查看 example-no-ui.md 以获取连接示例。

以下是一些代码片段:

初始化 Central Manager

通过调用 start()startStream() 来设置 central manager 并检查当前的 ble 状态。它们做的事情相同,但 startStream() 返回一个 AsyncStream,您可以使用它来监听 ble 状态的变化。

import AsyncCoreBluetooth

let centralManager = CentralManager()

// centralManager.start() if you don't need to listen for changes to the ble state
for await bleState in await centralManager.startStream() {
  switch bleState {
    case .unknown:
      print("Unkown")
    case .resetting:
      print("Resetting")
    case .unsupported:
      print("Unsupported")
    case .unauthorized:
      print("Unauthorized")
    case .poweredOff:
      print("Powered Off")
    case .poweredOn:
      print("Powered On, ready to scan")
  }
}

CentralManager 还通过其状态属性提供 @Observable 属性,用于 ble 状态,以便于与 SwiftUI 一起使用。

import AsyncCoreBluetooth

struct ContentView: View {
  var centralManager = CentralManager()
  var body: some View {
    NavigationStack {
      VStack {
        switch centralManager.state.bleState {
        case .unknown:
          Text("Unkown")
        case .resetting:
          Text("Resetting")
        case .unsupported:
          Text("Unsupported")
        case .unauthorized:
          Text("Unauthorized")
        case .poweredOff:
          Text("Powered Off")
        case .poweredOn:
            Text("Powered On, ready to scan")
        }
      }
      .padding()
      .navigationTitle("App")
    }
    .task {
      await centralManager.start()
      // or startStream if you want the async stream returned from start
    }
  }
}

您的应用程序应该处理 ble 状态的所有可能情况。有人关闭蓝牙或打开飞行模式非常常见,您的应用程序的 UI 应该反映这些状态。但是,最常见的情况是 .poweredOn,因此如果您只想在设备处于该状态时立即运行代码,则可以使用以下代码:

_ = await centralManager.startStream().first(where: {$0 == .poweredOn})

请记住,熟悉 Swift 并发将使使用此库变得更容易。

扫描或停止扫描外围设备

import AsyncCoreBluetooth

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

do {
  let peripherals = try await centralManager.scanForPeripherals(withServices: [heartRateServiceUUID])
  let peripheral = peripherals[heartRateServiceUUID]
  print("found peripheral \(peripheral)")
} catch {
  // This only happens when ble state is not powered on or you're already scanning
  print("error scanning for peripherals \(error)")
}

SwiftUI

import AsyncCoreBluetooth

struct ScanningPeripherals: View {
  let heartRateServiceUUID = UUID(string: "180D")
  var centralManager: CentralManager
  @MainActor @State private var peripherals: Set<Peripheral> = []

  var body: some View {
    VStack {
      List(Array(peripherals), id: \.identifier) { peripheral in
        Section {
          ScannedPeripheralRow(centralManager: centralManager, peripheral: peripheral)
        }
      }
    }
    .task {
      do {
        for await peripheral in try await centralManager.scanForPeripherals(withServices: [heartRateServiceUUID]) {
          peripherals.insert(peripheral)
          // break out of the loop or terminate the continuation to stop the scan
        }
      } catch {
        // This only happens when ble state is not powered on or you're already scanning
        print("error scanning for peripherals \(error)")
      }
    }
  }
}

建立或取消与外围设备的连接

CBCentralManager 一样,CentralManager 具有 connectcancelPeripheralConnection 方法。 但是,这两种方法都返回一个可丢弃的 AsyncStream,您可以使用它来监视外围设备的连接状态。

@discardableResult public func connect(_ peripheral: Peripheral, options: [String: Any]? = nil) async throws -> AsyncStream<Peripheral.ConnectionState> {
@discardableResult public func cancelPeripheralConnection(_ peripheral: Peripheral) async throws -> AsyncStream<Peripheral.ConnectionState> {
enum ConnectionState {
  case disconnected(CBError?)
  case connecting
  case connected
  case disconnecting
  case failedToConnect(CBError)
}

还有以下方法,您可以随时为外围设备获取一个 AsyncStream<Peripheral.ConnectionState>

func connectionState(forPeripheral peripheral: Peripheral) async -> AsyncStream<Peripheral.ConnectionState>

最重要的是,peripheral.state.connectionState@Observable,用于连接状态,以便于与 SwiftUI 一起使用。

let centralManager = CentralManager()

await centralManager.startStream().first(where: { $0 == .poweredOn })
print("Powered On, ready to scan")

let peripheral = try await centralManager.scanForPeripherals(withServices: nil).first()

连接

for await connectionState in await centralManager.connect(peripheral) {
  print(connectionState)
}

// or

await centralManager.connect(peripheral)
for await connectionState in centralManager.connectionState(forPeripheral: peripheral) {
  print(connectionState)
}

断开连接

for await connectionState in await centralManager.cancelPeripheralConnection(peripheral) {
  print(connectionState)
}

// or

await centralManager.cancelPeripheralConnection(peripheral)
for await connectionState in centralManager.connectionState(forPeripheral: peripheral) {
  print(connectionState)
}

您可以随意请求新的异步流或跳出这些流,而不会干扰外围设备的连接。 一旦您调用 connect,连接将由核心蓝牙正常管理。 您可以随时调用 cancelPeripheralConnection 以取消连接。

运行测试

swift test --no-parallel