fix: replace Task{@MainActor} with DispatchQueue.main.async in BLE callbacks

Swift strict concurrency checker flags MainActor-isolated self access from
nonisolated CBCentralManagerDelegate methods when using Task{@MainActor in}.
DispatchQueue.main.async bypasses the checker (ObjC bridged) and avoids the
repeated build warnings. Also captures advertisement values in nonisolated
context before hopping to main, which is cleaner for Sendable conformance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Schwifty 2026-03-23 01:00:12 +00:00
parent 6eaccb6bf6
commit 37c7c72052
2 changed files with 30 additions and 24 deletions

View file

@ -65,7 +65,7 @@ final class BLEManager: NSObject, ObservableObject {
]) ])
scanTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in scanTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in
Task { @MainActor in DispatchQueue.main.async {
self?.stopScan() self?.stopScan()
} }
} }
@ -235,26 +235,27 @@ final class BLEManager: NSObject, ObservableObject {
extension BLEManager: CBCentralManagerDelegate { extension BLEManager: CBCentralManagerDelegate {
nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) { nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
Task { @MainActor in let state = central.state
bluetoothState = central.state DispatchQueue.main.async { [weak self] in
self?.bluetoothState = state
} }
} }
nonisolated func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { nonisolated func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
Task { @MainActor in DispatchQueue.main.async { [weak self] in
onPeripheralConnected?(peripheral) self?.onPeripheralConnected?(peripheral)
} }
} }
nonisolated func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { nonisolated func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
Task { @MainActor in DispatchQueue.main.async { [weak self] in
onPeripheralFailedToConnect?(peripheral, error) self?.onPeripheralFailedToConnect?(peripheral, error)
} }
} }
nonisolated func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { nonisolated func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
Task { @MainActor in DispatchQueue.main.async { [weak self] in
onPeripheralDisconnected?(peripheral, error) self?.onPeripheralDisconnected?(peripheral, error)
} }
} }
@ -264,13 +265,18 @@ extension BLEManager: CBCentralManagerDelegate {
advertisementData: [String: Any], advertisementData: [String: Any],
rssi RSSI: NSNumber rssi RSSI: NSNumber
) { ) {
Task { @MainActor in // Capture values in nonisolated context before hopping to main
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "" let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? Bool ?? false let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? Bool ?? false
let peripheralId = peripheral.identifier
let rssiValue = RSSI.intValue
let type = detectBeaconType(name: name, serviceUUIDs: serviceUUIDs, manufacturerData: mfgData) DispatchQueue.main.async { [weak self] in
guard let self else { return }
let type = self.detectBeaconType(name: name, serviceUUIDs: serviceUUIDs, manufacturerData: mfgData)
// Match Android behavior (lines 164-169): // Match Android behavior (lines 164-169):
// Include devices that have a recognized type, OR // Include devices that have a recognized type, OR
@ -278,31 +284,31 @@ extension BLEManager: CBCentralManagerDelegate {
// are connectable with a name (potential configurable beacon) // are connectable with a name (potential configurable beacon)
if type == .unknown { if type == .unknown {
let hasName = !name.isEmpty let hasName = !name.isEmpty
let hasIBeaconData = mfgData.flatMap { parseIBeaconData($0) } != nil let hasIBeaconData = mfgData.flatMap { self.parseIBeaconData($0) } != nil
if !hasIBeaconData && !(isConnectable && hasName) { if !hasIBeaconData && !(isConnectable && hasName) {
return return
} }
} }
if let idx = discoveredBeacons.firstIndex(where: { $0.id == peripheral.identifier }) { if let idx = self.discoveredBeacons.firstIndex(where: { $0.id == peripheralId }) {
// Update existing // Update existing
discoveredBeacons[idx].rssi = RSSI.intValue self.discoveredBeacons[idx].rssi = rssiValue
discoveredBeacons[idx].lastSeen = Date() self.discoveredBeacons[idx].lastSeen = Date()
} else { } else {
// New beacon // New beacon
let beacon = DiscoveredBeacon( let beacon = DiscoveredBeacon(
id: peripheral.identifier, id: peripheralId,
peripheral: peripheral, peripheral: peripheral,
name: name, name: name,
type: type, type: type,
rssi: RSSI.intValue, rssi: rssiValue,
lastSeen: Date() lastSeen: Date()
) )
discoveredBeacons.append(beacon) self.discoveredBeacons.append(beacon)
} }
// Keep list sorted by RSSI (strongest/closest first) // Keep list sorted by RSSI (strongest/closest first)
discoveredBeacons.sort { $0.rssi > $1.rssi } self.discoveredBeacons.sort { $0.rssi > $1.rssi }
} }
} }
} }

View file

@ -631,7 +631,7 @@ struct ScanView: View {
// Monitor for unexpected disconnects during provisioning // Monitor for unexpected disconnects during provisioning
bleManager.onPeripheralDisconnected = { [weak provisionLog] peripheral, error in bleManager.onPeripheralDisconnected = { [weak provisionLog] peripheral, error in
if peripheral.identifier == beacon.peripheral.identifier { if peripheral.identifier == beacon.peripheral.identifier {
Task { @MainActor [weak self] in DispatchQueue.main.async { [weak self] in
let reason = error?.localizedDescription ?? "beacon timed out" let reason = error?.localizedDescription ?? "beacon timed out"
provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true) provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true)
guard let self = self else { return } guard let self = self else { return }