Currently I monitor Bluetooth status like this:
class One: NSObject {
private let centralManager = CBCentralManager()
init( ) {
super.init()
centralManager.delegate = self
}
}
extension One: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
NotificationCenter.default.post(
name: .xxx,
object: central.state)
}
}
And then another class listens to this notification, etc. Code above is simplified, as I think everything else is irrelevant, but let me know if something is missing from this picture.
This code works if bluetooth status is changed while application is running. But when application starts I am not getting any notification, so I don't know what is initial status of the Bluetooth.
So how do I get initial status of bluetooth on application startup?
Related
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
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)
}
}
Situation: i am developing an app, which manages HomeKit accessories. For example i do that:
Accessory is power on, and i see it via app. Also in foreground mode HMAccessoryDelegate method:
func accessoryDidUpdateReachability(HMAccessory)
works fine and i can handle status of my accessory.
I switch app to background mode.
I turn off accessory (i mean completely power off) so it must be unreachable.
I switch app to foreground mode, but accessory is still reachable.
method func accessoryDidUpdateReachability(HMAccessory) — not called.
value accessory.isReachable not updated.
Example of code when i go to foreground:
func applicationDidBecomeActive(_ application: UIApplication) {
if let home = HomeStore.sharedStore.home {
for accessory in home.accessories {
print(accessory.isReachable) //not updated
for service in accessory.services {
for characteristic in service.characteristics {
characteristic.readValue { (error) in //updated
if error == nil {
let notification = Notification(name: Notification.Name(rawValue: "UpdatedCharacteristic"))
NotificationCenter.default.post(notification)
}
}
}
}
}
}
}
Question: how to update isReachable values of accessories, when i come back from background mode to foreground?
You can create a function in the ViewController that implements HMHomeManagerDelegate:
func startHomeManager() {
manager = HMHomeManager()
manager.delegate = self
// do something here
}
and add a call to startHomeManager() to your viewDidLoad(). That will refresh your HMHome object. Then call this func in your AppDelegate:
func applicationWillEnterForeground(_ application: UIApplication) {
viewController.startHomeManager()
viewController.didRestartHomeManager()
}
A bonus is that you can call startHomeManager() for pull to refresh, etc.
In my app when I launch my app I want to check that device Bluetooth is on or off.
generally, we can get this in CBCentralManagerDelegate. Here is my Code
var manager:CBCentralManager!
in init() or viewDidLoad()
{
manager = CBCentralManager()
manager.delegate = self
}
// Delegate method
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
}
else if central.state == .resetting{
}
else if central.state == .unauthorized
{
}
else if central.state == .unknown
{
}
else if central.state == .unsupported
{
}
else if central.state == .poweredOff{
print("Bluetooth is not Connected.Please Enable it")
}
}
But the issue is that in this method we can get Bluetooth state only when the state is updated during app life cycle.
But we can not check at launch time without any state change.
So how can I get this at launch time to check that Bluetooth is on or off?
Shortly after you instantiate a CBCentralManager you will get a call to the centralManagerDidUpdateState delegate method with the current state. An explicit change in Bluetooth state is not required to trigger this behaviour.
I would suggest that you use appropriate CBCentralManager initialiser so that you can specify your delegate as part of the initialisation:
manager = CBCentralManager(delegate: self, queue: nil)
This is the only way to obtain Bluetooth power state.
There is no synchronous method that reports the current Bluetooth power state.
You need to update your UI/display an alert or take whatever other action you require in response to the delegate method call.
You can specify an option when you instantiate your CBCentralManager that causes iOS to show a system alert if Bluetooth is not currently on.
manager = CBCentralManager(delegate: self, queue: nil, options:
[CBCentralManagerOptionShowPowerAlertKey:1])
I am currently writing an app that connects via BLE to an external device. All operations are fine when the app is in foreground.....including connecting, obtaining data, and reconnecting (in cases of the device going out of range) via a reconnect protocol I wrote. The app also functions properly when it is backgrounded but the BLE connection remains alive.
However, the only instance in which the app does not function is if the app is backgrounded and then the BLE device goes out of range. Once the connection is broken, the app seems to be suspended by iOS after a few seconds and none of the code I wrote will continue to function...even if I bring the device back into range. The only way to restore functionality is to bring the app back into the foreground again. (Note: I have the info.plist file and all other settings configured appropriately for centralManager background functionality)
I've read some documentation and it seems that this comes down to not having state preservation/restore code properly implemented. I went ahead and implemented the "willRestoreState" and "didUpdateState" commands, but the app still doesn't reconnect to a device once it has been suspended when in background mode.
I've shown some relevant code below, including the willRestoreState, didUpdateState, and didDisconnect methods. Any ideas or suggestions? Thanks!
//define service+characteristic UUIDS
let serviceUUID = CBUUID(string: "xxxxxxxxx")
let streamingCharacteristicUUID = CBUUID(string: "xxxxxxxxx")
//Local dictionary of UUIDs for connected devices (the ble code updates this var with each connected device)
var devicesUniqueId:[UUID:String] = [UUID:String]()
//Local dictionary of connected peripherals, with respect to each of their UUIDS (the ble code updates this var with each connected device)
var sensorPeripheral = [UUID:CBPeripheral]()
///restoreState function
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
if let peripheralsObject = dict[CBCentralManagerRestoredStatePeripheralsKey] {
let peripherals = peripheralsObject as! Array<CBPeripheral>
print ("starting restorestate code")
if peripherals.count > 0 {
for i in 0 ..< peripherals.count {
print ("starting restorecheck")
//Check if the peripheral exists within our list of connected peripherals, and assign delegate if it does
if self.devicesUniqueId.keys.contains(peripherals[i].identifier) {
peripherals[i].delegate = self
}
}
}
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager)
{
if central.state != .poweredOn
{
return
}
self.startScanning()
//////Preservation + Restoration code////////
//Iterate through array of connected UUIDS
let keysArray = Array(self.patchDevicesUniqueId.keys)
for i in 0..<keysArray.count {
//Check if peripheral exists for given UUID
if let peripheral = self.sensorPeripheral[keysArray[i]] {
print("peripheral exists")
//Check if services exist within the peripheral
if let services = peripheral.services {
print("services exist")
//Check if predefined serviceUUID exists within services
if let serviceIndex = services.index(where: {$0.uuid == serviceUUID}) {
print("serviceUUID exists within services")
let transferService = services[serviceIndex]
let characteristicUUID = streamingCharacteristicUUID
//Check if predefined characteristicUUID exists within serviceUUID
if let characteristics = transferService.characteristics {
print("characteristics exist within serviceUUID")
if let characteristicIndex = characteristics.index(where: {$0.uuid == characteristicUUID}) {
print("characteristcUUID exists within serviceUUID")
let characteristic = characteristics[characteristicIndex]
//If characteristicUUID exists, begin getting notifications from it
if !characteristic.isNotifying {
print("subscribe if not notifying already")
peripheral.setNotifyValue(true, for: characteristic)
}
else {
print("invoke discover characteristics")
peripheral.discoverCharacteristics([characteristicUUID], for: transferService)
}
}
}
}
else {
print("invoke discover characteristics")
peripheral.discoverServices([serviceUUID])
}
}
}
}
}
//didDisconnect method to handle a connect command issue
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)
{
//commented out unnecessary code
self.removePeripheralData(peripheral: peripheral)
if(sensorCount>0){
sensorCount -= 1
}
}
//removePeripheralData function used in didDisconnect
func removePeripheralData ( peripheral: CBPeripheral) {
//Commented out unnecessary code
//Issue reconnect command
print ("issuing reconnect command")
centralManager.connect(peripheral, options: nil)
//Commented out unnecessary code
handleDidRemoveDevice()
}
I'm developing a BLE application for IOS (SWIFT) and I've found a strange behaviour.. my test has 2 controllers, ONE with the CentralManager Role and the other with the PeripheralManager Role..
Here's my code (summary):
Parameters.swift:
...
// a custome UUID created in console
let TRANSFER_SERVICE_UUID = CBUUID(string: "FB694B90-F49E-....-....-171BBA78F846")
...
Peripheral.swift
...
var pManager = CBPeripheralManager()
var transferService = CBMutableService()
override func viewDidLoad() {
super.viewDidLoad()
pManager = CBPeripheralManager(delegate: self, queue: nil)
}
func peripheralManagerDidUpdateState(peripheral: CBPeripheralManager!) {
if(peripheral.state == CBPeripheralManagerState.PoweredOn) {
transferService = CBMutableService(type: TRANSFER_SERVICE_UUID, primary: true)
// add some characteristic
pManager.addService(transferService)
pManager.startAdvertising(nil)
}
}
...
Central.swift
...
var cManager = CBCentralManager()
override func viewDidLoad() {
super.viewDidLoad()
cManager = CBCentralManager(delegate: self, queue: nil)
}
func centralManagerDidUpdateState(central: CBCentralManager!) {
if central.state == CBCentralManagerState.PoweredOn {
cManager.scanForPeripheralsWithServices([TRANSFER_SERVICE_UUID], options: nil)
}
}
...
Now, if I take 2 device, one with the Central and the other with the Peripheral Role the 2 app can't find each other (but LightBlue app and similar will so the device is emitting)
On the other hand, if I change the code to:
cManager.scanForPeripheralsWithServices(nil, options: nil)
my application works perfectly and the 2 devices can communicate each other.. but at the same time I can't filter only the devices that are emitting TRANSFER_SERVICE_UUID.. I don't want to connect to all peripheral finded in order to search for TRANSFER_SERVICE_UUID.. isn't this the right way to proceed ?? Did I miss something ??
There is only limited space in the Bluetooth advertisement area, so iOS does not automatically advertise all services - A device may have a primary service and several supplementary services. Advertising all services is wasteful when all that is required is to discover the primary service in order to identify a candidate device.
To be able to discover the service in scanForPeripheralsWithServices you need to include the service in the advertisement data.
This is done by including the service's UUID in the dictionary you pass to CBPeripheralManager.startAdvertising -
pManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey:[TRANSFER_SERVICE_UUID]])