There are a few Stack Overflow questions about this however they are either outdated or have mixed answers on whether 2 central managers are supported. I am using IOS 16.2, the problem also occurs on IOS 16.1. Xcode version 14.1.
PROBLEM: When attempting to initialize multiple CBCentralManagers, only the first CBCentralManager calls the centralManagerDidUpdateState() function to update its state, the other managers have their state always at unknown.
DESIRED STATE: All central managers call the centralManagerDidUpdateState() function of their respective delegates. In the code provided, the second central manager should print "SECOND MANAGER UPDATED STATE."
BACKGROUND: The reason I am using multiple central managers is because I am using a package that initializes its own Core Bluetooth central manager (https://github.com/NordicSemiconductor/IOS-nRF-Mesh-Library/blob/main/nRFMeshProvision/Classes/Bearer/GATT/BaseGattProxyBearer.swift#L109). I noticed the code wasn't doing what it was supposed to (and documented to do). The documentation states that the object in the link I provided above can be initialized within the didDiscover function of a user defined central manager's delegate (https://github.com/NordicSemiconductor/IOS-nRF-Mesh-Library/blob/main/Documentation.docc/nRFMeshProvision.md#provisioning). I investigated enough to find the problem lies in having multiple central managers. A workaround I can pursue is using only 1 central manager and passing it to the package, but that would require me to edit the source and perhaps lead to more cumbersome unsupported workarounds with it. The package was documented to be working in September of 2022, so I don't know if Apple changed the way Core Bluetooth CentralManagers behave to only allow 1 working at a time, or if there may be something wrong with my environment.
Here is my code to replicate the issue easily.
CODE:
ModelData, where the central managers are initialized:
import Foundation
import Combine
import CoreBluetooth
final class ModelData: {
let centralManager: CBCentralManager!
let secondCentralManager: CBCentralManager!
init() {
let centralManagerDelegate = CentralManagerDelegate()
centralManager = CBCentralManager(delegate: centralManagerDelegate, queue: nil)
let secondCMDelegate = SecondCentralManagerDelegate()
secondCentralManager = CBCentralManager(delegate: secondCMDelegate, queue: nil)
print("initialized central managers")
}
}
Central Manager Delegate:
import Foundation
import CoreBluetooth
class CentralManagerDelegate: NSObject, ObservableObject, CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .unknown:
print("Bluetooth Device is UNKNOWN")
case .unsupported:
print("Bluetooth Device is UNSUPPORTED")
case .unauthorized:
print("Bluetooth Device is UNAUTHORIZED")
case .resetting:
print("Bluetooth Device is RESETTING")
case .poweredOff:
print("Bluetooth Device is POWERED OFF")
case .poweredOn:
print("Bluetooth Device is POWERED ON")
@unknown default:
print("Unknown State")
}
}
}
Second Central Manager Delegate:
import Foundation
import CoreBluetooth
class SecondCentralManagerDelegate: NSObject, ObservableObject, CBCentralManagerDelegate {
func centralManagerDidUpdateState(\_ central: CBCentralManager) {
print("SECOND MANAGER UPDATED STATE")
}
}
The view calling the modelData:
struct HomeScreen: View {
@StateObject private var modelData = ModelData()
@State private var showDetail = true
var body: some View {
}
}
Output when running:
initialized central managers
Bluetooth Device is POWERED ON
CodePudding user response:
You are creating your delegate objects as local variables and since the CBCentralManager
doesn't retain its delegate, these will be released as soon as init
returns. You are probably just lucky that the first delegate has an opportunity to print the state before it is released. The second delegate is released too soon.
You can easily confirm this by adding a deinit
method to your delegate classes that prints something - You will see your delegates being de-initialised.
The solution is to use a property to retain a strong reference to your delegates as you do with the CBCentralManager
instances
final class ModelData: {
let centralManager: CBCentralManager!
let secondCentralManager: CBCentralManager!
let centralManagerDelegate: CentralManagerDelegate
let secondCMDelegate: SecondCentralManagerDelegate
init() {
centralManagerDelegate = CentralManagerDelegate()
centralManager = CBCentralManager(delegate: centralManagerDelegate, queue: nil)
secondCMDelegate = SecondCentralManagerDelegate()
secondCentralManager = CBCentralManager(delegate: secondCMDelegate, queue: nil)
}
}