why CBCenterManager! is set to Implicitly unwrapped

Hello everyone! I'm currently working on an iOS app developed with Swift that involves connecting to a specific bluetooth device and exchanging data even when the app is terminated or running in the background.

I just want to understand why the CBCenterManager should be Implicitly unwrapped to use it. I have check out couple off apple developer sample project it was set to Implicitly unwrapped. can some one help to understand the reason behind this, also what are possible issues/scenario trigger if we set the centermanager to optional "CBcentermanager?"

Thanks in Advance!

Replies

I’m not 100% sure I understand your question, but I think it relates to Swift’s definitive initialisation story. Consider this code:

 1 import CoreBluetooth
 2 
 3 class Test: NSObject, CBCentralManagerDelegate {
 4 
 5     override init() {
 6         self.central = nil
 7         super.init()
 8         self.central = CBCentralManager(delegate: self, queue: .main)
 9     }
10     
11     var central: CBCentralManager?
12     
13     func centralManagerDidUpdateState(_ central: CBCentralManager) {
14         print(self.central)
15     }
16 }

In Swift, you have to initialise all of your properties before you let self ‘escape’. So, you hit a chicken’n’egg problem:

  • You have to pass self to CBCentralManager.init(…) on line 8.

  • But you can’t do that until you’ve initialised self.central.

There are a variety of ways you can get around this. In this example I’ve made self.central optional. That’s a hassle though, because then all the folks using central have to handle that optional. So, a common approach is to make it an implicitly wrapped optional.

In theory this introduces a race condition:

  1. On line 8 you call CBCentralManager.init(…), passing it self as the delegate. The central property remains nil until that initialiser returns.

  2. Before it returns, it calls a delegate method.

  3. That delegate method accesses central, which is still nil, which crashes.

In practice, you can avoid that race by running this code on the same serial queue that you pass to CBCentralManager.init(…). In this example that’s the main queue. Any delegate method call must happen on the main queue, but the main queue is busy until the initialiser returns, so the delegate method can’t happen until the initialiser returns on line 9.


Another way to get around this is to split the delegate out into a separate object:

import CoreBluetooth

class Test: NSObject {

    override init() {
        self.delegate = CentralDelegate()
        self.central = CBCentralManager(delegate: self.delegate, queue: .main)
        super.init()
        self.delegate.didUpdateState = self.centralDidUpdateState
    }

    private let delegate: CentralDelegate
    private let central: CBCentralManager
    
    private func centralDidUpdateState() {
        print(central)
    }

    private class CentralDelegate: NSObject, CBCentralManagerDelegate {
        
        var didUpdateState: (() -> Void)? = nil
        
        func centralManagerDidUpdateState(_ central: CBCentralManager) {
            self.didUpdateState?()
        }
    }
}

However, that comes with significant drawbacks:

  • It’s more complex.

  • If you hit the race I’ve talked about above, it just ignores the callback rather than crashing. That makes things harder to debug.

  • The code, as written, introduces a retain loop. You can get around that, but it makes things even more complicated.


Honestly, I think the implicitly unwrapped optional is the better approach. That’s what I use for URLSession, which has the same problem as CBCentralManager.

Of course, the best solution is to craft an API that doesn’t have this problem. Remember that these APIs significantly predate Swift.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"