iOS: Connect via Bluetooth (Native)
Note: Demo: Native iOS example (WKWebView + CoreBluetooth) → native-ios-example This guide builds on the Low-Level Transport Plugin and its 64-byte message protocol—read that first.
This page shows how to integrate @onekeyfe/hd-common-connect-sdk in a native iOS host via a low-level adapter. The JavaScript bundle runs in WKWebView; transport calls are forwarded to native CoreBluetooth and bridged back to JS.
Key stack:
- CoreBluetooth (system)
- WKWebViewJavascriptBridge (CocoaPods) for JS ↔ Native messaging
Info.plist (permissions):
NSBluetoothAlwaysUsageDescription- (Older iOS)
NSBluetoothPeripheralUsageDescription
OneKey BLE UUIDs:
- Service:
00000001-0000-1000-8000-00805f9b34fb - Write:
00000002-0000-1000-8000-00805f9b34fb - Notify:
00000003-0000-1000-8000-00805f9b34fb
Step 1. Pod and web assets
Podfile (add the bridge dependency):
platform :ios, '13.0'
use_frameworks!
target 'YourAppTarget' do
pod 'WKWebViewJavascriptBridge'
endBuild the web bundle (already implemented in the demo):
# inside hardware-js-sdk repo
cd packages/connect-examples/native-ios-example/web
yarn && yarn build # emits web/web_dist/Copy web assets into your app target resources (two options):
- Option A (keep folder; recommended for clarity):
- Copy the entire
web/web_dist/folder into your Xcode project (e.g., a group namedweb/web_dist) and ensure it is included in “Copy Bundle Resources”. - Load path:
web/web_dist/index.html.
- Copy the entire
- Option B (flatten):
- Copy all files inside
web/web_dist/directly into your app bundle root (or a chosen assets folder). - Load the matching path (e.g.,
index.html).
- Copy all files inside
Keep the HTML <script src="..."> paths consistent with your placement.
Step 2. State and handler names
class ViewController: UIViewController {
// Web
var webView: WKWebView!
var bridge: WKWebViewJavascriptBridge!
// BLE
var manager: CBCentralManager!
var peripheral: CBPeripheral?
var writeCharacteristic: CBCharacteristic?
var notifyCharacteristic: CBCharacteristic?
// Service UUID
let serviceID = "00000001-0000-1000-8000-00805f9b34fb"
// Bridge callbacks (enumeration etc.)
var enumerateCallback: ((Any?) -> Void)?
}Step 3. WKWebView + Bridge + load HTML (initialization order matters)
- Create the WKWebView
- Create the bridge
- Register all native handlers (enumerate/connect/disconnect/send/monitorCharacteristic)
- Load the HTML (from the bundled
web/web_dist/)
import CoreBluetooth
import WebKit
import WKWebViewJavascriptBridge
class ViewController: UIViewController {
// ... (state omitted for brevity)
override func viewDidLoad() {
super.viewDidLoad()
manager = CBCentralManager(delegate: self, queue: .main)
webView = WKWebView(frame: view.bounds)
view.addSubview(webView)
bridge = WKWebViewJavascriptBridge(webView: webView)
registerBridgeHandlers()
// Load built HTML bundle (Option A path shown)
if let htmlPath = Bundle.main.path(forResource: "index", ofType: "html", inDirectory: "web/web_dist") {
webView.load(URLRequest(url: URL(fileURLWithPath: htmlPath)))
}
}
}Step 4. Bridge handlers (enumerate / connect / disconnect / send)
extension ViewController {
func registerBridgeHandlers() {
// enumerate: scan BLE and return [{ id, name }]
bridge.register(handlerName: "enumerate") { [weak self] _, callback in
guard let self = self else { return }
self.enumerateCallback = callback
self.manager.scanForPeripherals(
withServices: [CBUUID(string: self.serviceID)], options: nil
)
// Stop after a short window in production; see demo for accumulation and de-duplication
}
// connect: connect to a specific peripheral UUID
bridge.register(handlerName: "connect") { [weak self] params, callback in
guard
let self = self,
let uuid = params?["uuid"] as? String,
let id = UUID(uuidString: uuid)
else {
callback?(["success": false, "error": "Invalid UUID"])
return
}
// Try to retrieve a known peripheral first
if let found = self.manager.retrievePeripherals(withIdentifiers: [id]).first {
self.peripheral = found
self.manager.connect(found, options: nil)
} else {
// Fallback: scan and match during didDiscover
self.manager.scanForPeripherals(
withServices: [CBUUID(string: self.serviceID)], options: nil
)
}
callback?(["success": true])
}
// disconnect
bridge.register(handlerName: "disconnect") { [weak self] _, callback in
if let p = self?.peripheral { self?.manager.cancelPeripheralConnection(p) }
callback?(["success": true])
}
// send: write hex payload
bridge.register(handlerName: "send") { [weak self] params, callback in
guard
let self = self,
let hex = params?["data"] as? String,
let ch = self.writeCharacteristic
else { callback?(["success": false]); return }
var bytes = [UInt8]()
var index = hex.startIndex
while index < hex.endIndex {
let next = hex.index(index, offsetBy: 2)
if let b = UInt8(hex[index..<next], radix: 16) { bytes.append(b) }
index = next
}
self.peripheral?.writeValue(Data(bytes), for: ch, type: .withoutResponse)
callback?(["success": true])
}
}
}Step 5. CoreBluetooth scanning and notifications
extension ViewController: CBCentralManagerDelegate, CBPeripheralDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
// Handle .poweredOn / other states; optionally restore a cached device by UUID
}
// Accumulate devices and return to JS
func centralManager(
_ central: CBCentralManager,
didDiscover p: CBPeripheral,
advertisementData: [String : Any],
rssi RSSI: NSNumber
) {
let item: [String: String] = [
"id": p.identifier.uuidString,
"name": p.name ?? ""
]
// Return to JS enumerate callback (store callback during enumerate)
enumerateCallback?( [item] )
// In production: de-duplicate and stop scan after timeout or enough results
}
func centralManager(_ central: CBCentralManager, didConnect p: CBPeripheral) {
p.delegate = self
p.discoverServices([CBUUID(string: serviceID)])
}
func peripheral(_ p: CBPeripheral, didDiscoverServices error: Error?) {
guard let service = p.services?.first else { return }
let writeID = CBUUID(string: "00000002-0000-1000-8000-00805f9b34fb")
let notifyID = CBUUID(string: "00000003-0000-1000-8000-00805f9b34fb")
p.discoverCharacteristics([writeID, notifyID], for: service)
}
func peripheral(_ p: CBPeripheral, didDiscoverCharacteristicsFor s: CBService, error: Error?) {
s.characteristics?.forEach { ch in
if ch.uuid == CBUUID(string: "00000002-0000-1000-8000-00805f9b34fb") { writeCharacteristic = ch }
if ch.uuid == CBUUID(string: "00000003-0000-1000-8000-00805f9b34fb") {
notifyCharacteristic = ch
p.setNotifyValue(true, for: ch)
}
}
}
func peripheral(_ p: CBPeripheral, didUpdateValueFor ch: CBCharacteristic, error: Error?) {
guard let data = ch.value else { return }
// Forward hex to JS — the web bundle reassembles frames and resolves receive()
let hex = data.map { String(format: "%02x", $0) }.joined()
bridge.callHandler("monitorCharacteristic", data: hex)
}
}Step 6. JavaScript bundle (low-level adapter)
The demo’s web project (under native-ios-example/web) already builds a bundle that initializes the SDK with env: 'lowlevel' and wires the low-level adapter. You typically do NOT need to write extra JS — just build and include the web/web_dist/ directory in your app resources so it can be loaded (e.g., web/web_dist/index.html).
If you customize the adapter, keep the same pattern: initialize with env: 'lowlevel' and forward enumerate/connect/disconnect/send/receive to the native bridge.
Step 7. (Optional) Native UI prompts bridging
If you want to present native PIN/confirmation UI instead of handling dialogs only in JS, expose extra handlers so JS can request native UI and receive results. The demo shows this pattern.
// Example: PIN input handler bridging (simplified)
bridge.register(handlerName: "requestPinInput") { [weak self] _, callback in
// Present your native PIN screen.
// Return "" (empty) to instruct JS to use device PIN entry,
// or return a transformed PIN string (blind keypad sequence) for software entry.
// For example, to force on-device input:
callback?("")
}
// Example: confirmation dialog
bridge.register(handlerName: "requestButtonConfirmation") { _, callback in
// Show a native confirm dialog; return "ok" or "cancel" as needed.
callback?("ok")
}
bridge.register(handlerName: "closeUIWindow") { _, callback in
// Close any native overlay.
callback?("closed")
}In JS, you would call these handlers from your adapter to mirror the demo’s behavior. Otherwise, you can handle UI entirely in JS using UI_EVENT — see Config Event for event wiring and responses (WebUSB guide includes minimal dialogs).
Step 8. Checklist
- Register handlers before loading the HTML to avoid race conditions.
- Scan with service UUID filter and stop within a reasonable time window.
- Persist
connectId(peripheral UUID) and fetchdevice_idwithgetFeatures(connectId)after the first connection. - Always subscribe to
UI_EVENTin JS to avoid stalled requests (see Config Event). - Keep
web/web_dist/in your app bundle and adjust theloadpath accordingly.
References
- Message Protocol (64‑byte framing): See Low-Level Transport Plugin
- Low‑level transport plugin contract: Low-level Transport Plugin