Corebluetooth Scanning BGAppRefreshTask not working - ios

I am working on a project for scanning BLE devices all around in the foreground and there is no issue. It is working well!
But background I cannot make scanning. I did everything on info.plist and on the section "Signing&Capabilities", switch on the Background Model for "Background Fetch".
I read almost everything on the Web about Corebluetooth, BGAppRefreshTask, Background Modes but find nothing.
Only thing I find get advertisement data of the BLE devices around me. UUID and other things are not important for me. I only need to identify BLE devices and know that it is same in other iOS device when it also look for it.
If you have any other solution to guide me to go another way, it will appreciated.
Here is the code of BLECentral. The .swift file which I want to call in Background.
import Foundation
import CoreBluetooth
class BLECentral: NSObject, CBCentralManagerDelegate {
var manager: CBCentralManager!
var discoveredPeripherals = [DiscoveredPeripheral]()
var onDiscovered: (()->Void)?
override init() {
super.init()
manager = CBCentralManager(delegate: self, queue: nil)
}
func scanForPeripherals() {
let options: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: false]
manager.scanForPeripherals(withServices: nil, options: options)
print("ben tarıyom")
}
// MARK: - CBCentralManagerDelegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
scanForPeripherals()
} else {
print("central is unavailable: \(central.state.rawValue)")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if let existingPeripheral = discoveredPeripherals.first(where: {$0.peripheral == peripheral}) {
existingPeripheral.advertisementData = advertisementData
existingPeripheral.rssi = RSSI
print(existingPeripheral.advertisementData)
} else {
discoveredPeripherals.append(DiscoveredPeripheral(peripheral: peripheral, rssi: RSSI, advertisementData: advertisementData))
}
onDiscovered?()
}
}
Here is the AppDelegate code:
import UIKit
import CoreBluetooth
import BackgroundTasks
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
var central: BLECentral!
var manager: CBCentralManager!
var window: UIWindow?
var flowController: AppFlowController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
flowController = AppFlowController(window: window!)
flowController?.start()
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.blebacktrial.refresh", using: nil) { (task) in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
func handleAppRefresh(task: BGAppRefreshTask) {
central.scanForPeripherals()
scheduleAppRefresh()
}
// MARK: - Scheduling Tasks
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.blebacktrial.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 60) // Fetch no earlier than 1 minutes from now
do {
try BGTaskScheduler.shared.submit(request)
print("task scheduled")
} catch {
print("Could not schedule app refresh: \(error)")
}
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
}

You don't need to use a background refresh task. Scanning for BLE peripherals is a supported background mode in its own right.
However, you can't scan for service nil in the background. You must scan for the specific service that you are interested in.
From the documentation
Your app can scan for Bluetooth devices in the background by specifying the bluetooth-central background mode. To do this, your app must explicitly scan for one or more services by specifying them in the serviceUUIDs parameter. The CBCentralManager scan option has no effect while scanning in the background.

Related

How to get notify when advertisementData changed when app is in background?

I want to get notified when something happened at the BLE device.
If BLE device passes some data/Command to the app, then in-app the advertisementData not changed.
But the same thing we can do with android it's working perfectly.
I want to implement functionality like when advertisementData changed I want to get notify.
Please help me to implement this.
Below is my AppDelegate.swift class.
private var centralManager : CBCentralManager!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
return true
}
func applicationWillEnterForeground(_ application: UIApplication) {
print("entering foreground...")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("entered background...")
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Bluetooth is On")
let kTrackStandardDeviceInfoServiceUUID = CBUUID(string: "180A")
let dictionaryOfOptions = [CBCentralManagerScanOptionAllowDuplicatesKey : true]
let arrayOfServices: [CBUUID] = [kTrackStandardDeviceInfoServiceUUID]
centralManager.scanForPeripherals(withServices: arrayOfServices, options: dictionaryOfOptions)
} else {
print(central.state)
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("\nName : \(peripheral.name ?? "(No name)")")
print("RSSI : \(RSSI)")
let name = peripheral.name ?? ""
if name.contains("ETGuardian") {
let DetailData = advertisementData["kCBAdvDataManufacturerData"]
let DiscoveredData = String(describing: DetailData)
print(DiscoveredData)
for ad in advertisementData {
print("AD Data: \(ad)")
}
}
}
For having the application can work in the background, you need to implement some background services in your apps.
Usually, background service is location fetch.
Please note that background service will make your app draining the battery faster
To implement the background service, Click your project, Signing & Capabilities, Background Modes, enable the BLE features.

How to scan BLE devices in Background?

I want to scan BLE devices on the background as like a foreground. But my iOS app doesn't work as I expect. Below is my AppDelegate class code.
private var centralManager : CBCentralManager!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)return true
}
func applicationWillEnterForeground(_ application: UIApplication) {
print("entering foreground...")
}
func applicationDidEnterBackground(_ application: UIApplication) {
print("entered background...")
print(centralManager.state)
centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Bluetooth is On")
centralManager.scanForPeripherals(withServices: nil, options: nil)
} else {
print(central.state)
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
print("\nName : \(peripheral.name ?? "(No name)")")
print("RSSI : \(RSSI)")
let name = peripheral.name ?? ""
if name.contains("ETGuardian") {
let DetailData = advertisementData["kCBAdvDataManufacturerData"]
let DiscoveredData = String(describing: DetailData)
print(DiscoveredData)
for ad in advertisementData {
print("AD Data: \(ad)")
}
}
}
}
Please help me to scan app in background state as like a foreground.
From Docs: link
Apps that have specified the bluetooth-central background mode are
allowed to scan while in the background. That said, they must
explicitly scan for one or more services by specifying them in the
serviceUUIDs parameter. The CBCentralManager scan option is ignored
while scanning in the background.
For background scanning to work you need to specify serviceUUIDs.

AppDelegate Never Gets Its didReceiveRemoteNotification Called For CKQuerySubscription

I'm trying to let the iOS app listen to CKQuerySubscription changes. Data is transmitted by a remote iOS app. I already have a macOS application, which does receive data sent by the remote iOS app. The iOS app I have trouble with already has a subscription. Yet, its AppDelegate never receives a call in the didReceiveRemoteNotification method.
import UIKit
import UserNotifications
import CloudKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
/* notifications */
let center = UNUserNotificationCenter.current()
center.delegate = self
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
switch settings.authorizationStatus {
case .authorized:
print("You already have permission")
DispatchQueue.main.async() {
application.registerForRemoteNotifications()
}
case .denied:
print("setting has been disabled")
case .notDetermined:
print("Let me ask")
UNUserNotificationCenter.current().requestAuthorization(options: []) { (granted, error) in
if error == nil {
if granted {
print("you are granted permission")
DispatchQueue.main.async() {
application.registerForRemoteNotifications()
}
}
}
}
}
}
return true
}
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register notifications_ error:", error)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("Receiving data...") // never called...
}
}
I have some capabilities on as shown below. I don't know if the app needs push notifications. For now, it's turned on.
So why doesn't my iOS app get the remote notification call? I'm using the app with an actual device, not a simulator. Thanks.
EDIT: Creating a subscription to a record change
class HomeViewController: UIViewController {
override func viewDidLoad() {
registerSubscription()
}
func registerSubscription() {
let cloudContainer = CKContainer(identifier: "iCloud.com.xxx.XXXXX")
let privateDB = cloudContainer.privateCloudDatabase
let predicate = NSPredicate(format: "TRUEPREDICATE")
let subscription = CKQuerySubscription(recordType: "PrivateRecords", predicate: predicate, options: .firesOnRecordCreation)
let notification = CKNotificationInfo()
subscription.notificationInfo = notification
privateDB.save(subscription, completionHandler: ({returnRecord, error in
if let err = error {
print("Subscription has failed: \(err.localizedDescription)")
} else {
print("Subscription set up successfully")
print("Subscription ID: \(subscription.subscriptionID)")
}
}))
}
}
There are a few more things you can check.
First, make sure you implement didReceiveRemoteNotification in your app delegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
let dict = userInfo as! [String: NSObject]
let notification = CKNotification(fromRemoteNotificationDictionary: dict)
if let sub = notification.subscriptionID{
print("iOS Notification Received: \(sub)")
}
}
There are also a few other things you can check:
Try deleting your CKQuerySubscription in the CloudKit dashboard, then run your iOS code again that registers it. Does the subscription show up in the dashboard?
Does the CloudKit log show that a notification was sent? It lists all notifications that were pushed to a device.
If you are using silent push notifications, try enabling Background fetch in the Background Modes capability (right above Remote notifications).
If you do all that and it still doesn't work, can you share your CKQuerySubscription code?
-- Update --
Try setting some additional attributes on your CKNotificationInfo object. There are some obscure bugs with notifications that can usually be circumvented by setting a couple properties like this:
notification.shouldSendContentAvailable = true
notification.alertBody = "" //(Yes, a blank value. It affects the priority of the notification delivery)
You can also try setting your predicate to: NSPredicate(value: true)
Also, what does your privateDB.save method return? Does it say it succeeds or fails?

Can a CBCentralManager extend custom Protocol? Can we override CBCentralManagerDelegate?

// Custom class
public protocol BluetoothManagerProtocol {
var delegate: CBCentralManagerDelegate? {get set}
//var state: CBCentralManagerState { get }
func scanForPeripheralsWithServices(serviceUUIDs: [CBUUID]?, options: [String : AnyObject]?)
func stopScan()
func connectPeripheral(peripheral: CBPeripheral, options: [String : AnyObject]?)
func connectPeripheral(peripheral: BluetoothPeripheral, options: [String : AnyObject]?)
}
extension CBCentralManager : BluetoothManagerProtocol {
public func connectPeripheral(peripheral: CBPeripheral, options: [String : AnyObject]?) {
//
}
public func scanForPeripheralsWithServices(serviceUUIDs: [CBUUID]?, options: [String : AnyObject]?) {
//
}
public func connectPeripheral(peripheral: BluetoothPeripheral, options: [String : AnyObject]?) {
guard let peripheral = peripheral as? CBPeripheral else {
return
}
connectPeripheral(peripheral, options: options)
}
}
extension CBCentralManagerDelegate{
func centralManager(central: BluetoothManagerProtocol, didDiscoverPeripheral peripheral: BluetoothPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) {}
func centralManager(central: BluetoothManagerProtocol,didConnectPeripheral peripheral:BluetoothPeripheral) {}
func centralManagerDidUpdateState(central: BluetoothManagerProtocol) {}
func centralManager(central: BluetoothManagerProtocol, didDisconnectPeripheral peripheral: BluetoothPeripheral, error: NSError?) {}
}
I just jumped in to advanced topics of Protocols and Delegates in Swift.I am surprised, can a CBCentralManager extend custom Protocol?
How can CBCentralManagerDelegate methods can be parametrized with the custom Protocol?
Whats the concept behind this? And what exactly is the need?
This was written in swift 2.3. Will this strategy work in Swift 4.0?
Yes, CBCentralManager is from Core Bluetooth framework and can be made to include custom protocol definitions. This approach is followed to leverage TDD - Test Driven Development.
As unit testing Bluetooth functionality makes it difficult while syncing devices, developers take advantage of dependency injection to mock methods by creating their own custom methods instead of using the methods provided by Apple for iOS Frameworks.
You can include your own custom methods for UIView, UIColor etc..
For example
class MockCBCentralManager : BluetoothManagerProtocol {
var delegate: CBCentralManagerDelegate?
var scanCalled: Bool = false
var connectPeripheralCalled = false
fileprivate var internalState: CBCentralManagerState = .unknown
var state: CBCentralManagerState {
get {
return internalState
}
}
}
func scanForPeripheralsWithServices(_ serviceUUIDs: [CBUUID]?, options[String : AnyObject]?)
{
scanCalled = true
let advertisementData =
[CBAdvertisementDataServiceUUIDsKey :
[STUDENT_APP_UUID],CBAdvertisementDataLocalNameKey:"MockPeripheral"]
let mock = MockPeripheral()
(delegate as? CentralManager)?.centralManager(self,
didDiscoverPeripheral: mock, advertisementData:
advertisementData,RSSI: 90)
}
More information can be found at
https://nomothetis.svbtle.com/the-ghost-of-swift-bugs-future
Cheers!

BLE background processing not working in iOS application

I have an application that needs to interact with peripherals when the app enters a suspended or terminated state. I've added the bluetooth-central to the Info.plist and Capabilities (image) but after i connect to a peripheral it doesn't fire any of the delegate methods while running in the background, but as soon as the app enters the foreground the delegate methods are called! I've also set up CBCenteralManager state restoration
manager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: CoreBluetoothRestorationKey])
let ids = [CBUUID(string: ServiceUUID)]
manager.scanForPeripheralsWithServices(ids, options: nil)
manager.connectPeripheral(peripheral, options: nil)
I've been digging around the internet for hours and haven't found anything. Has anyone had a similar situation before? Any tips? Thanks!
edit: code in context:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let bluetoothManager = CoreBluetoothObserver()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
return true
}
}
class CoreBluetoothObserver: NSObject {
let manager: CBCentralManager
override init() {
manager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: CoreBluetoothRestorationKey])
super.init()
manager.delegate = self
}
}
the CoreBlueToothObserver() initializes CBCenteralManager in its init method and sets self to its delegate. I would assume CoreBlueToothObserver() would be around for the lifetime of the app and not need to be re-initalized in didFinishLaunchingWithOptions? or maybe i need to recreate a CBCenteralManager and inject it in didFinishLaunchingWithOptions? Just stumped as to whats happening.
With the help from #Paulw11 this worked
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var bluetoothManager: CoreBluetoothObserver!
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
bluetoothManager = CoreBluetoothObserver()
return true
}
}
app is now receiving the delegate methods!

Resources