Home > Back-end >  Multiple CBCentralManagers in the same app not calling their update state functions
Multiple CBCentralManagers in the same app not calling their update state functions

Time:12-20

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)
    }

}
  • Related