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:
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:
-
On line 8 you call CBCentralManager.init(…)
, passing it self
as the delegate. The central
property remains nil
until that initialiser returns.
-
Before it returns, it calls a delegate method.
-
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"