iOS: Connect via Bluetooth
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-00805f9b34fbWrite:
00000002-0000-1000-8000-00805f9b34fbNotify:
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.
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).
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): OneKey Message Protocol
Low‑level transport plugin contract: Low-level Transport Plugin
Last updated
Was this helpful?