Article summary
If you’re implementing Bluetooth support in an iOS app, you will inevitably find the Core Bluetooth Programming Guide. It shows up in the results for just about any search containing “corebluetooth” as a keyword. It’s linked from many parts of the Apple Bluetooth documentation. And yet, when you land on the programming guide, a 10-year-old+ design and the heading “Documentation Archive” greets you. Your first reaction might be to go back and try to find updated documentation. But this is it! Somehow Bluetooth on iOS remains largely unchanged since 2013-09-18 (the “Updated” date at the bottom of that page).
I’m not complaining. Bluetooth on iOS is actually pretty solid. But it could be useful to know a few details the programming left out, especially concerning background Bluetooth. If you’ve already been through enabling your app to function as a Bluetooth “central”, it’s probably felt like a lot already. Thinking about the additional steps to enable background Bluetooth might sound like too much. But it’s not as bad as it sounds!
Background Bluetooth
The first step is to opt-in since iOS is generally pretty hostile toward apps doing work in the background. It’s not going to help you out unless it’s really sure you want it. Fortunately, this is pretty simple (especially if you only have one CBCentralManager
instance for your whole app, which seems like a common case): just check the “Uses Bluetooth LE accessories” option under the project’s “Background Modes” in Xcode, and then use the CBCentralManagerOptionRestoreIdentifierKey
option when instantiating your CBCentralManager
. This part is covered in the programming guide, so nothing too confusing there.
Resurrection
So remember how I said iOS is pretty hostile toward apps doing stuff in the background? I was surprised to read that, when you’ve opted into Bluetooth state restoration, iOS will relaunch your app in the background to handle Bluetooth events (even if it was previously terminated)! This is pretty neat, even if it has a significant drawback: it won’t work if the user has force-quit your app.
Problem
That’s fair, but Apple has also made the interface for force-quitting an app far too innocuous. Many users are in the habit of “closing” their apps when they’re done, not realizing the havoc it causes for developers.
Anyway, if your app is lucky enough to stay lurking around in the background, it has a better chance of receiving Bluetooth events. But, if it is killed, conditions must be just right for iOS to relaunch it. Those conditions are:
- The app terminated “gracefully” (e.g. the user left it in the background, and iOS needed to terminate it due to memory pressure)
Okay, so I guess that’s just one condition. But put another way, there are a lot of reasons iOS will not relaunch your app:
- The user force-quit the app.
- Bluetooth has been turned off (even if it has been turned back on).
- The user restarted the iOS device.
- iOS terminated the app for exceeding limits (e.g. a background task did not complete within the time allowed).
Solution
This sounds awfully nebulous and potentially difficult to test. (What, start some other memory-hog app and hope iOS terminates mine gracefully?!) But it turns out to be much easier than it sounds since stopping the app while running it in the Xcode debugger counts as a “graceful” termination. If you do this, your app should start running again within a few minutes (or sooner, if your Bluetooth peripheral emits a notification or other Bluetooth event).
But since iOS has launched your app in the background, and the Xcode debugger is no longer attached, it will not be obvious that your app is running again. One way to check is to view the list of running processes on an attached iOS device. You can see this in Xcode under the Debug menu -> Attach to Process.
State Restoration
Once you’ve convinced iOS to relaunch your app in the background as much as possible, the next daunting step is state restoration. This line from the programming guide is a doozy:
After you have reinstantiated the appropriate central and peripheral managers in your app, restore them by synchronizing their state with the state of the Bluetooth system.
Delegate Method
According to the guide, the first step is to implement the CBCentralManagerDelegate
‘s willRestoreState:
method. This delegate method gives your app a little bag of state you’re supposed to use to “synchronize with the Bluetooth system.” Mostly it contains peripherals that iOS has kept track of for you.
The guide sounds like this is your one chance to restore things like connected peripherals. But it turns out that’s not the case! It’s true that willRestoreState:
is called first, before any other delegate methods, but you don’t have to do anything with it. If you already had code reconnecting to a known peripheral using retrievePeripheralWithIdentifier:
, you can still just do that and iOS will give you the restored peripheral.
This means its state might already be connected
. It might already have services
with characteristics
. Those characteristics might already be notifying. So you might not have to do anything further to support state restoration. But you’ll probably want to, to minimize background processing (don’t tempt iOS to terminate your app). You’ll also save battery for everyone — the iOS device and the Bluetooth device).
Another Tweak
Since the restored peripheral likely already has discovered services and characteristics, there’s no need to do it again. But it’s not an all-or-nothing affair. The programming guide calls out another unfortunate reality:
“your app may have been terminated while it was in the middle of exploring the data of a connected peripheral”
But since a CBPeripheral
always keeps track of the services and characteristics it has discovered and which ones are notifying, this is also not as bad as it seems. Your code can simply avoid discovering things that have already been discovered and check isNotifying
on a characteristic before calling setNotifyValue:
on the peripheral. The best-case scenario is that you don’t have to do any discovery or reconfigure any notifications!
Connection Management
An important thing about Bluetooth is that iOS is really the one managing the connection. It uses the terms of “connect” and “disconnect” with your app, even if that’s not totally accurate. For example, disconnecting a peripheral only disconnects your app. The physical connection might still exist if other apps are still using it.
But this works in your favor because it means your app can go to sleep in the background and iOS will wake it up when something meaningful happens with the bluetooth device. That means notifications for subscribed characteristics, but it also includes connection events. So if the user’s phone wanders out of range of the bluetooth device, your app will wake up and receive a didDisconnectPeripheral:
callback via the CBCentralManagerDelegate
.
What you do with that callback is up to you, but the best thing is to turn around and connect
the peripheral. Connection requests never time out. That means your app will go back to sleep while iOS keeps an eye out for the device to come back within range. As soon as it does, you’ll get a didConnectPeripheral:
callback.
Leveraging Background Bluetooth in an iOS App
Of course, battery usage is always top of mind for mobile devices. iOS plays some undocumented tricks to spend less power on Bluetooth operations, especially with no foreground apps using Bluetooth. So, don’t expect to reconnect as fast as you would while actively scanning. But, as daunting as it may seem, background Bluetooth is not hard to implement and works well.