Best Practice: Bluetooth Low Energy in Different Platforms
Bluetooth Low Energy (BLE) is a core technology behind fitness trackers, smart home devices, medical equipment, and many other IoT products. When building a BLE-enabled app, you often face a choice: native iOS, Flutter, or React Native?
Rather than relying on third-party BLE libraries for Flutter or React Native, the approach I recommend — and practice — is to write all BLE logic in native Swift using CoreBluetooth, then expose it to each cross-platform framework via its native bridge mechanism. For React Native, that means Native Modules. For Flutter, that means Platform Channels.
This gives you full control of the BLE stack, consistent behavior across all your projects, and zero dependency on external BLE packages that may lag behind iOS SDK updates.
The idea is simple: keep CoreBluetooth as the single source of truth for BLE, and treat React Native / Flutter as the UI layer that communicates with it.
The native layer handles scanning, connecting, discovering services, reading and writing characteristics. The cross-platform layer only needs to call a method or listen to an event stream.
1. The Native BLE Layer (CoreBluetooth)
This code is shared and reused across all platforms. A clean BLEManager class encapsulates all CoreBluetooth logic.
Setup & Permissions
Add to Info.plist:
1 2
<key>NSBluetoothAlwaysUsageDescription</key> <string>This app uses Bluetooth to connect to your device.</string>
For background scanning, also add to Info.plist and enable the capability in Xcode:
// Large payload: wait for this before sending the next chunk funcperipheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { // Resume chunked write } }
This BLEManager is the foundation. Now let’s expose it to each platform.
2. Bridging to React Native
React Native communicates with native code via Native Modules. You create a Swift class that:
Inherits from RCTEventEmitter to push events (scan results, data) to JavaScript.
Exposes methods via @objc and an Objective-C .m bridge file.
Requires bluetooth-central background mode + state restore key
Same as native iOS — must be configured natively
State restoration
CBCentralManagerOptionRestoreIdentifierKey
Same — pass to BLEManager
Same — handled in native layer
Boilerplate effort
Low
Medium (.m bridge file needed)
Medium (channel setup in AppDelegate)
BLE logic location
Swift
Swift (bridged)
Swift (bridged)
The key insight: BLE logic is identical across all three. Only the bridge layer differs.
5. Best Practices
General
Always filter by service UUID when scanning. An unfiltered scan returns every visible device and drains the battery.
Stop scanning as soon as you’ve found your target device.
Hold a strong reference to CBPeripheral. If it is deallocated, the connection silently drops.
Cache characteristics after discovery. Never rediscover on every read/write.
Test on a real device — CoreBluetooth does not work in the iOS Simulator.
Writing Data
Use .withResponse for commands that require acknowledgment.
Use .withoutResponse for streaming/high-throughput writes, but always wait for peripheralIsReady(toSendWriteWithoutResponse:) before the next chunk.
Check peripheral.maximumWriteValueLength(for:) before sending and chunk your data accordingly.
1 2 3 4 5 6 7 8 9
funcsend(data: Data, to peripheral: CBPeripheral, char: CBCharacteristic) { let mtu = peripheral.maximumWriteValueLength(for: .withoutResponse) var offset = 0 while offset < data.count { let end = min(offset + mtu, data.count) peripheral.writeValue(data[offset..<end], for: char, type: .withoutResponse) offset = end } }
Background BLE
Add bluetooth-central to UIBackgroundModes.
Always use a CBCentralManagerOptionRestoreIdentifierKey so iOS can re-launch your app and restore state after it was terminated in the background.
Implement centralManager(_:willRestoreState:) to re-attach to previously connected peripherals.
Keep background delegate callbacks short — long tasks will get your app killed.
React Native Specific
Always removeAllListeners for BLE events when the component unmounts to avoid memory leaks.
Rather than depending on third-party BLE libraries that abstract away CoreBluetooth, building a thin native Swift layer and bridging it gives you:
Full access to every CoreBluetooth feature (state restoration, background modes, MTU control, etc.)
Consistency — the same BLE logic serves your native iOS, React Native, and Flutter apps
Stability — you’re not blocked by an unmaintained third-party package when a new iOS version ships
The bridge overhead is small: a .m file and some @objc decorators for React Native, and a MethodChannel + EventChannel setup for Flutter. In exchange, you get a BLE layer you fully own and understand.