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:
parent
6eaccb6bf6
commit
37c7c72052
2 changed files with 30 additions and 24 deletions
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue