From 37c7c720523638de0f567e04f0029f7fcf1ca46f Mon Sep 17 00:00:00 2001 From: Schwifty Date: Mon, 23 Mar 2026 01:00:12 +0000 Subject: [PATCH] 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) --- PayfritBeacon/Services/BLEManager.swift | 52 ++++++++++++++----------- PayfritBeacon/Views/ScanView.swift | 2 +- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/PayfritBeacon/Services/BLEManager.swift b/PayfritBeacon/Services/BLEManager.swift index 2255166..3bcc522 100644 --- a/PayfritBeacon/Services/BLEManager.swift +++ b/PayfritBeacon/Services/BLEManager.swift @@ -65,7 +65,7 @@ final class BLEManager: NSObject, ObservableObject { ]) scanTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in - Task { @MainActor in + DispatchQueue.main.async { self?.stopScan() } } @@ -235,26 +235,27 @@ final class BLEManager: NSObject, ObservableObject { extension BLEManager: CBCentralManagerDelegate { nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) { - Task { @MainActor in - bluetoothState = central.state + let state = central.state + DispatchQueue.main.async { [weak self] in + self?.bluetoothState = state } } nonisolated func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - Task { @MainActor in - onPeripheralConnected?(peripheral) + DispatchQueue.main.async { [weak self] in + self?.onPeripheralConnected?(peripheral) } } nonisolated func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - Task { @MainActor in - onPeripheralFailedToConnect?(peripheral, error) + DispatchQueue.main.async { [weak self] in + self?.onPeripheralFailedToConnect?(peripheral, error) } } nonisolated func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - Task { @MainActor in - onPeripheralDisconnected?(peripheral, error) + DispatchQueue.main.async { [weak self] in + self?.onPeripheralDisconnected?(peripheral, error) } } @@ -264,13 +265,18 @@ extension BLEManager: CBCentralManagerDelegate { advertisementData: [String: Any], rssi RSSI: NSNumber ) { - Task { @MainActor in - let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "" - let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] - let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data - let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? Bool ?? false + // Capture values in nonisolated context before hopping to main + let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "" + let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] + let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data + 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): // Include devices that have a recognized type, OR @@ -278,31 +284,31 @@ extension BLEManager: CBCentralManagerDelegate { // are connectable with a name (potential configurable beacon) if type == .unknown { let hasName = !name.isEmpty - let hasIBeaconData = mfgData.flatMap { parseIBeaconData($0) } != nil + let hasIBeaconData = mfgData.flatMap { self.parseIBeaconData($0) } != nil if !hasIBeaconData && !(isConnectable && hasName) { return } } - if let idx = discoveredBeacons.firstIndex(where: { $0.id == peripheral.identifier }) { + if let idx = self.discoveredBeacons.firstIndex(where: { $0.id == peripheralId }) { // Update existing - discoveredBeacons[idx].rssi = RSSI.intValue - discoveredBeacons[idx].lastSeen = Date() + self.discoveredBeacons[idx].rssi = rssiValue + self.discoveredBeacons[idx].lastSeen = Date() } else { // New beacon let beacon = DiscoveredBeacon( - id: peripheral.identifier, + id: peripheralId, peripheral: peripheral, name: name, type: type, - rssi: RSSI.intValue, + rssi: rssiValue, lastSeen: Date() ) - discoveredBeacons.append(beacon) + self.discoveredBeacons.append(beacon) } // Keep list sorted by RSSI (strongest/closest first) - discoveredBeacons.sort { $0.rssi > $1.rssi } + self.discoveredBeacons.sort { $0.rssi > $1.rssi } } } } diff --git a/PayfritBeacon/Views/ScanView.swift b/PayfritBeacon/Views/ScanView.swift index 416e0ec..e92dc8a 100644 --- a/PayfritBeacon/Views/ScanView.swift +++ b/PayfritBeacon/Views/ScanView.swift @@ -631,7 +631,7 @@ struct ScanView: View { // Monitor for unexpected disconnects during provisioning bleManager.onPeripheralDisconnected = { [weak provisionLog] peripheral, error in if peripheral.identifier == beacon.peripheral.identifier { - Task { @MainActor [weak self] in + DispatchQueue.main.async { [weak self] in let reason = error?.localizedDescription ?? "beacon timed out" provisionLog?.log("disconnect", "Unexpected disconnect: \(reason)", isError: true) guard let self = self else { return }