自定义BLE设备反复出现在配对列表并尝试连接的iOS适配问题
Hey there, let's tackle this annoying BLE reconnection issue you're seeing on older iOS devices (iPhone 6s iOS 11.2.1, iPhone 5s iOS 10.2.2). After diving into your BluetoothManager code, I've spotted several key gaps that are likely triggering the constant reconnection attempts, plus concrete fixes to resolve them.
What's Causing the Problem?
Let's walk through the main culprits in your code:
1. Unrestricted BLE Scanning
In centralManagerDidUpdateState, you're scanning for all BLE devices (using withServices: nil) with duplicate detections enabled:
central.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true])
Older iOS versions don't handle connected device filtering well—this means your app will keep detecting the same device even after it's connected, triggering the didDiscover method to retry the connection over and over.
2. Incomplete State Cleanup After Disconnect
- Your
didDisconnectPeripheralmethod only logs the disconnect, but doesn't clear theconnectedDevicereference, stop pending send timers, or reset send state (segments,currentSegment). Older iOS systems hold onto stale peripheral references and automatically try to reconnect them. - The
disconnectmethod doesn't null out the peripheral's delegate, leaving open callbacks that can trigger unintended reconnections.
3. Unmanaged Send Timers
You're using a repeating Timer to send segmented data, but there's no proper cleanup for it:
- If the device disconnects or data finishes sending, the Timer might keep running and try to write to a disconnected peripheral—this triggers the system's automatic reconnection logic.
- The Timer holds references to
selfand the peripheral, creating potential retain cycles and keeping the system thinking the device is active.
4. Aggressive Notification State Handling
In didUpdateNotificationStateFor, you immediately cancel the connection if notifications are disabled:
guard characteristic.isNotifying else { manager?.cancelPeripheralConnection(peripheral) return }
On older iOS versions, this abrupt disconnect can trigger the system to try re-establishing the connection automatically.
Step-by-Step Fixes
Let's fix each issue one by one:
1. Restrict Scanning to Your Target Device
Scan only for your device's specific service UUID (replace YOUR_SERVICE_UUID with your actual service ID) and disable duplicate detections unless you need real-time RSSI updates:
func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .poweredOn: // Only scan for YOUR device's service, no duplicates let targetService = CBUUID(string: "YOUR_SERVICE_UUID") central.scanForPeripherals(withServices: [targetService], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) // ... keep other state handling } }
2. Clean Up State Properly on Disconnect
Update didDisconnectPeripheral to clear all stale state:
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { print("OS Disconnect : \(String(describing: error?.localizedDescription))") // Clear state if this is our connected device if let connectedPeriph = connectedDevice?.peripheral, connectedPeriph == peripheral { connectedDevice = nil segments = nil currentSegment = 0 sendTimer?.invalidate() sendTimer = nil } }
And update disconnect to null out the peripheral's delegate and clean up all resources:
func disconnect() { completion = nil notFoundTimer?.invalidate() notFoundTimer = nil manager?.stopScan() guard let connectedDevice = self.connectedDevice else { return } // Null out delegate to stop callbacks connectedDevice.peripheral.delegate = nil connectedDevice.peripheral.setNotifyValue(false, for: connectedDevice.characteristic) manager?.cancelPeripheralConnection(connectedDevice.peripheral) // Clear all connection/send state self.connectedDevice = nil segments = nil currentSegment = 0 sendTimer?.invalidate() sendTimer = nil }
3. Manage Send Timer Lifecycle
Add a property to track the send Timer, and ensure it's stopped when needed:
class BluetoothManager:NSObject { // Add this property to track the send timer var sendTimer: Timer? // ... rest of your properties }
Then update the timer creation and send logic:
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let error = error { self.completeWithResponse(response:.fail(error: error.localizedDescription)) return } if !characteristic.isNotifying { // Clean up instead of abrupt disconnect connectedDevice = nil sendTimer?.invalidate() sendTimer = nil return } // Start send timer only if there's data to send guard let segments = segments, !segments.isEmpty else { return } sendTimer?.invalidate() // Stop any existing timer sendTimer = Timer.scheduledTimer( timeInterval: 0.05, target: self, selector: #selector(send(sender:)), userInfo: (peripheral: peripheral, characteristic: characteristic), repeats: true ) } @objc internal func send(sender:Timer) { guard let userInfo = sender.userInfo as? (peripheral: CBPeripheral, characteristic: CBCharacteristic), let segments = segments, currentSegment < segments.count else { // Stop timer if no more data or invalid state sender.invalidate() sendTimer = nil return } userInfo.peripheral.writeValue(segments[currentSegment], for: userInfo.characteristic, type: .withoutResponse) currentSegment += 1 // Stop timer once all segments are sent if currentSegment >= segments.count { sender.invalidate() sendTimer = nil } }
4. Soften Notification State Handling
Instead of immediately canceling the connection when notifications are disabled, clean up state first. You can optionally trigger a reconnection if needed:
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let error = error { self.completeWithResponse(response:.fail(error: error.localizedDescription)) return } if !characteristic.isNotifying { // Clean up state gracefully connectedDevice = nil sendTimer?.invalidate() sendTimer = nil // Optional: Trigger a reconnection if your app requires it // manager?.connect(peripheral, options: nil) return } // Proceed with sending data as before guard let segments = segments, !segments.isEmpty else { return } sendTimer?.invalidate() sendTimer = Timer.scheduledTimer( timeInterval: 0.05, target: self, selector: #selector(send(sender:)), userInfo: (peripheral: peripheral, characteristic: characteristic), repeats: true ) }
5. Add Older iOS Specific Checks
In iOS 10/11, the central manager can re-trigger scans unexpectedly. Add a check to only scan when there's no active connection:
func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .poweredOn: // Only scan if we don't have an active connection if connectedDevice == nil { let targetService = CBUUID(string: "YOUR_SERVICE_UUID") central.scanForPeripherals(withServices: [targetService], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) } // ... keep other state handling } }
How to Verify the Fixes
- Test on the problematic devices (iPhone 5s iOS 10.2.1, iPhone 6s iOS 11.2.1) to confirm the reconnection loop stops.
- Use Xcode's Debug > Debug Workflow > View Bluetooth to monitor active connections—ensure no stale connections remain after disconnecting.
- Test the full flow: connect, send data, disconnect, and check that no automatic reconnection attempts happen.
内容的提问来源于stack exchange,提问作者Christos Chadjikyriacou

