在 iOS 与智能硬件(手环、传感器、控制模块等)交互中,BLE(Bluetooth Low Energy)是最常用的通信方式。本文将基于 CoreBluetooth + Swift ,给出一套工程可用的连接外设代码,并总结开发中最常遇到的注意事项。
适用场景:BLE 设备连接、读写特征、订阅通知、接收回包、断线重连。
一、准备工作
1)Info.plist 权限配置(必须)
iOS 13+ 起必须给出蓝牙使用说明,否则扫描/连接会失败或系统拒绝。
swift
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要使用蓝牙连接外设进行数据通讯</string>
如果你还需要后台持续蓝牙通信:需要 capability + plist 配置(后文注意事项会讲)。
2)导入框架
swift
import CoreBluetooth
二、BLE 连接的标准流程
- 初始化
CBCentralManager - 蓝牙开启后开始扫描
- 找到目标外设并连接
- 发现 Service
- 发现 Characteristic
- 找到写特征(Write)与通知特征(Notify)
- 开启通知并开始收发数据
三、代码:BluetoothManager(可直接用)
下面给出一个轻量但工程化的 BLE 管理器,支持:
- 按名称/服务 UUID 过滤
- 扫描超时
- 连接成功后自动发现服务/特征
- 自动开启 Notify
- 写入数据(支持写入响应/无响应)
- 断开回调(可扩展重连)
- 简单的写入节流(避免写太快导致丢包) ``
你只需要把 UUID 替换成你设备的即可。
swift
import Foundation
import CoreBluetooth
final class BluetoothManager: NSObject {
static let shared = BluetoothManager()
// MARK: - Public callbacks (按需扩展)
var onStateChanged: ((CBManagerState) -> Void)?
var onDiscovered: ((CBPeripheral, NSNumber) -> Void)?
var onConnected: ((CBPeripheral) -> Void)?
var onDisconnected: ((CBPeripheral, Error?) -> Void)?
var onReceiveData: ((Data, CBCharacteristic) -> Void)?
// MARK: - CoreBluetooth
private var central: CBCentralManager!
private(set) var peripheral: CBPeripheral?
private var writeChar: CBCharacteristic?
private var notifyChar: CBCharacteristic?
// MARK: - Config (替换为你的设备 UUID)
/// 推荐:用 Service UUID 过滤扫描,效率更高、结果更准
private let targetServiceUUID = CBUUID(string: "FFF0")
private let writeCharUUID = CBUUID(string: "FFF1")
private let notifyCharUUID = CBUUID(string: "FFF2")
/// 可选:按名称过滤(如果设备名称稳定)
private let targetNamePrefix = "MyBLE"
// MARK: - Scan control
private var scanTimer: Timer?
private let scanTimeout: TimeInterval = 10
// MARK: - Write throttle
private var writeQueue: [Data] = []
private var isWriting = false
private override init() {
super.init()
// queue 建议用串行队列,避免回调并发导致状态错乱
let queue = DispatchQueue(label: "com.tangbin.ble.queue")
central = CBCentralManager(delegate: self, queue: queue)
}
// MARK: - Public APIs
/// 开始扫描
func startScan() {
guard central.state == .poweredOn else { return }
stopScan()
// 只扫目标 Service:更省电更精准(强烈推荐)
central.scanForPeripherals(withServices: [targetServiceUUID], options: [
CBCentralManagerScanOptionAllowDuplicatesKey: false
])
startScanTimeoutTimer()
}
/// 停止扫描
func stopScan() {
if central.isScanning {
central.stopScan()
}
scanTimer?.invalidate()
scanTimer = nil
}
/// 连接外设
func connect(_ p: CBPeripheral) {
stopScan()
peripheral = p
peripheral?.delegate = self
central.connect(p, options: [
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true
])
}
/// 主动断开
func disconnect() {
guard let p = peripheral else { return }
central.cancelPeripheralConnection(p)
}
/// 发送数据(写入队列节流)
func send(_ data: Data, withResponse: Bool = false) {
guard let p = peripheral, let w = writeChar else { return }
let type: CBCharacteristicWriteType = withResponse ? .withResponse : .withoutResponse
// 如果写入无响应,也建议做节流,避免外设来不及处理
writeQueue.append(data)
pumpWriteQueue(peripheral: p, characteristic: w, type: type)
}
// MARK: - Private
private func startScanTimeoutTimer() {
scanTimer?.invalidate()
scanTimer = Timer.scheduledTimer(withTimeInterval: scanTimeout, repeats: false) { [weak self] _ in
self?.stopScan()
}
RunLoop.main.add(scanTimer!, forMode: .common)
}
private func pumpWriteQueue(peripheral p: CBPeripheral,
characteristic c: CBCharacteristic,
type: CBCharacteristicWriteType) {
guard !isWriting else { return }
guard !writeQueue.isEmpty else { return }
isWriting = true
let packet = writeQueue.removeFirst()
// 注意:此处写入发生在 central 的队列上
p.writeValue(packet, for: c, type: type)
// withoutResponse 的情况下不会走 didWriteValueFor,所以用延迟释放
if type == .withoutResponse {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.02) { [weak self] in
self?.isWriting = false
self?.pumpWriteQueue(peripheral: p, characteristic: c, type: type)
}
}
}
}
// MARK: - CBCentralManagerDelegate
extension BluetoothManager: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
onStateChanged?(central.state)
if central.state == .poweredOn {
// 可按需自动扫描
// startScan()
} else {
// 蓝牙关闭/不可用时要清空状态
stopScan()
peripheral = nil
writeChar = nil
notifyChar = nil
}
}
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber) {
// 如果还想按名称做二次过滤
if let name = peripheral.name, name.hasPrefix(targetNamePrefix) {
onDiscovered?(peripheral, RSSI)
connect(peripheral)
return
}
// 不按名称过滤也行:只要服务 UUID 已经过滤,通常就很准
onDiscovered?(peripheral, RSSI)
connect(peripheral)
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
onConnected?(peripheral)
// 连接后发现服务
peripheral.discoverServices([targetServiceUUID])
}
func centralManager(_ central: CBCentralManager,
didFailToConnect peripheral: CBPeripheral,
error: Error?) {
onDisconnected?(peripheral, error)
}
func centralManager(_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?) {
onDisconnected?(peripheral, error)
// 可选:重连策略(示例:延迟重连)
// DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
// central.connect(peripheral, options: nil)
// }
}
}
// MARK: - CBPeripheralDelegate
extension BluetoothManager: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard error == nil else { return }
guard let services = peripheral.services else { return }
for s in services {
if s.uuid == targetServiceUUID {
peripheral.discoverCharacteristics([writeCharUUID, notifyCharUUID], for: s)
}
}
}
func peripheral(_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?) {
guard error == nil else { return }
guard let chars = service.characteristics else { return }
for c in chars {
if c.uuid == writeCharUUID { writeChar = c }
if c.uuid == notifyCharUUID { notifyChar = c }
}
// 开启通知(Notify)
if let n = notifyChar {
peripheral.setNotifyValue(true, for: n)
}
// 可选:读取一次初始值
// if let n = notifyChar { peripheral.readValue(for: n) }
}
func peripheral(_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?) {
// 通知开关状态回调
}
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
guard error == nil else { return }
let data = characteristic.value ?? Data()
onReceiveData?(data, characteristic)
}
func peripheral(_ peripheral: CBPeripheral,
didWriteValueFor characteristic: CBCharacteristic,
error: Error?) {
// 只有 withResponse 才会进这个回调
isWriting = false
if let w = writeChar {
pumpWriteQueue(peripheral: peripheral, characteristic: w, type: .withResponse)
}
}
}
四、调用示例(在 ViewController 里)
swift
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let ble = BluetoothManager.shared
ble.onStateChanged = { state in
print("BLE state:", state.rawValue)
if state == .poweredOn {
ble.startScan()
}
}
ble.onConnected = { p in
print("已连接:", p.name ?? "unknown")
}
ble.onReceiveData = { data, ch in
let hex = data.map { String(format: "%02x", $0) }.joined()
print("收到数据((ch.uuid)):", hex)
}
}
func sendCommand() {
// 举例:发送一段 hex 指令
let hex = "aabbccdd"
let data = Data(hexString: hex) // 见下方扩展
BluetoothManager.shared.send(data, withResponse: false)
}
}
也可以复用我之前写的 ParseDataTool(Swift版)来做 Hex/Data 转换。
五、附:Data 十六进制扩展(可选)
swift
extension Data {
init(hexString: String) {
let clean = hexString.replacingOccurrences(of: " ", with: "")
var data = Data()
var idx = clean.startIndex
while idx < clean.endIndex {
let next = clean.index(idx, offsetBy: 2)
let byteStr = clean[idx..<next]
if let b = UInt8(byteStr, radix: 16) {
data.append(b)
}
idx = next
}
self = data
}
}
六、开发注意事项(非常关键)
1)强烈建议:扫描时指定 Service UUID
更快、更省电、更准确
避免扫描全部 nil 会扫到大量无关设备,影响体验
swift
central.scanForPeripherals(withServices: [targetServiceUUID], options: nil)
2)不要用 peripheral.name 当唯一标识
name 可能为空、可能变化。更靠谱的是:
- 过滤 Service UUID
- 使用
peripheral.identifier(UUID)做缓存识别(重连)
3)写数据别太快,否则丢包/外设卡死
BLE 写入速度过快常见问题:
- 外设缓冲区溢出
- 回包延迟或丢失
- iOS 侧 write 被吞
建议做写入节流(本文示例已经做了队列 + 20ms 间隔)
4)区分 withResponse / withoutResponse
.withResponse:可靠,有回调didWriteValueFor.withoutResponse:速度快,但无写入确认,建议配合队列节流
实战建议:
- 协议关键指令用
.withResponse - 大数据(如 OTA)用
.withoutResponse+ 节流 + 外设 ACK
5)Notify 要记得开启(很多人漏掉)
外设回包多数走 Notify(通知),不打开你永远收不到数据:
swift
peripheral.setNotifyValue(true, for: notifyChar)
6)断线是常态:要做重连策略
断线原因很多:
- 超距
- 外设省电休眠
- 手机锁屏/系统资源回收
建议:
didDisconnectPeripheral里做延迟重连- 或 UI 提示用户手动重连
- 结合
peripheral.identifier记住上次设备
7)后台蓝牙通信(可选)
如果你需要锁屏/后台持续通信:
- Xcode → Signing & Capabilities → Background Modes → 勾选 Uses Bluetooth LE accessories
- 并合理控制扫描/连接行为(后台会更耗电)
8)MTU 与分包问题
- BLE 默认有效载荷常见为 20 字节(不同设备协商后可能变大)
- 大数据(日志、图片、OTA)一定要做分包 + 协议确认
最后
本文给出了一套 Swift BLE 连接外设的我开发成熟项目过程中的代码,可直接运用在项目中,覆盖了:
- 初始化、扫描、连接
- 服务/特征发现
- Notify 开启、收包回调
- 写入(带队列节流)
- 断开处理与重连扩展
如有写错的地方,敬请指正,相互学习进步,谢谢~