Enabling Background BLE Scanning on iPhone


A previous post shows how to create a simple BLE (Bluetooth Low Energy) scanner app for iPhone. The app can detect BLE devices while the app is in foreground. The goal of this post is to create an app that scans even it goes to the background.

 

Prerequisites (parentheses indicate my environment)

  • Xcode (10.0) ruining on Mac (10.14)
  • iPhone (iPhone 8 with iOS 12.0.1)
    * An actual iOS device is required since Bluetooth is not supported in Xcode simulator.
  • A BLE peripheral device to be scanned (Galaxy S7 running BLE Peripheral Simulator app [1])

 

Steps
1. Launch Xcode and create a Single View App.

2. Select Info.plist in Project navigator.

3. Click on “+” button next to “Information Property List” and select “Required Background Modes”.

4. Expand “Required Background Modes” by clicking on the triangle icon.

5. Add “App communicates using CoreBluetooth” as the value.

6. Open ViewController.swift file and import CoreBluetooth framework.

import CoreBluetooth

7. Add CBCentralManagerDelegate protocol to ViewController class.

class ViewController: UIViewController, CBCentralManagerDelegate {

8. Then, Xcode will prompt an error. Click on “Fix” button and it will create  centralManagerDidUpdateState() method which will be called when the central manager’s state is changed. At startup, this method is called after instantiating CBCentralManager.

9. Declare a variable for CBCentralManager in ViewController class.

private var centralManager : CBCentralManager!

10. Instantiate CBCentralManager in viewDidLoad().

centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)

11. In centralManagerDidUpdateState() method, start scanning when the state is poweredOn.

if central.state == .poweredOn {
    print("Bluetooth is On")
    centralManager.scanForPeripherals(withServices: [CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB")], options: nil)
} else {
    print("Bluetooth is not active")
}

In order to scan in background, service UUIDs need to be specified [2]. In this post, the app will scan for Battery Service [3] for testing purpose (Line 3).

8. Define centralManager didDiscover method. It will be called when any advertising device is discovered. Inside the method, it prints the device name, RSSI, and advertising data.

public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
    print("\nName   : \(peripheral.name ?? "(No name)")")
    print("RSSI   : \(RSSI)")
    for ad in advertisementData {
        print("AD Data: \(ad)")
    }
}

9. Open AppDelegate.swift file and add print statements in applicationDidEnterBackground method and applicationWillEnterForeground method to output the app state transitions.

func applicationDidEnterBackground(_ application: UIApplication) {
    print("entered background.")
}
    
func applicationWillEnterForeground(_ application: UIApplication) {
    print("entering foreground.")
}

 

Test
1. Background Scan
1-1. Make sure below conditions before start.

  • The peripheral device is not advertising yet.
  • Bluetooth is ON on the iPhone.

1-2. Connect iPhone to Mac.

1-3. Build and run the program.

1-4. Lock the iPhone screen by pressing the side button.

1-5. Start advertising on the peripheral device. In case of BLE Peripheral Simulator, launch the app and tap on “Battery”.

Below is the example of the result of background scan.

Bluetooth is On
entered background.

Name   : (No name)
RSSI   : -59
AD Data: (key: "kCBAdvDataTxPowerLevel", value: -7)
AD Data: (key: "kCBAdvDataIsConnectable", value: 1)
AD Data: (key: "kCBAdvDataServiceUUIDs", value: <__NSArrayM 0x2807f01b0>(Battery))

Notice that the Bluetooth device name is not detected. Let’s scan in foreground and compare the results.

 

2. Foreground Scan
2-1. Stop advertising on the peripheral device by tapping Back button.

2-2. Cancel screen lock on iPhone.

2-3. Restart advertising on the peripheral device.

entering foreground.

Name   : (No name)
RSSI   : -69
AD Data: (key: "kCBAdvDataTxPowerLevel", value: -7)
AD Data: (key: "kCBAdvDataIsConnectable", value: 1)
AD Data: (key: "kCBAdvDataServiceUUIDs", value: <__NSArrayM 0x281e09770>(Battery))

Name   : Galaxy S7
RSSI   : -69
AD Data: (key: "kCBAdvDataLocalName", value: Galaxy S7)
AD Data: (key: "kCBAdvDataServiceUUIDs", value: <__NSArrayM 0x281e09950>(Battery))
AD Data: (key: "kCBAdvDataTxPowerLevel", value: -7)
AD Data: (key: "kCBAdvDataIsConnectable", value: 1)

In above, didDiscover peripheral callback was called twice (Line 3-7 and Line 9-14). AD Data of the first result is same as the background scan result. The second result additionally has a device name in AD Data (Line 11).
According to an Apple Developer Forum post [4], the first result is from the initial Advertising packet and the second is from Scan Response packet. And the second packet is not guaranteed when the iOS app is running in background. That explains why the background scan result didn’t have the second packet.
So, when scanning in background, some data may not be available (e.g. in this case, the device name).

 

Takeaway
BLE scan while app is in background (even when screen is locked) is possible. However, there are some limitations need to be consider when writing an app.

  • Service UUIDs need to be specified when starting BLE scan.
  • Some data may not be received. (i.e. Data in Scan Response)
  • Other limitations are describe in Apple’s official document [5].

 

References
[1] BLE Peripheral Simulator – Google Play Store
[2] scanForPeripherals(with Services: options:) – Apple Developer
[3] GATT Specifications Battery Service – Bluetooth SIG
[4] Apple Developer Forums – Apple Developer
[5] Core Bluetooth Background Execution Modes – Apple Developer

 

 

 

Sponsor Link

2 Comments

  1. Hello Dear ,

    I am getting Can’t end BackgroundTask: no background task exists with identifier 2 (0x2), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug. when application run in foreground it’s scan near all device but when I go background it is not working.
    Please let me know how I handle it.
    Please I request to you help me.

Comments are closed.