Skip to main content

iOS End-to-End Integration

SDK Version: 1.0.0
Products: XTalkBleSdk, XTalkDeviceAuthSdk

This session demonstrates the recommended customer ownership graph and complete flow. UI calls public actions and renders @Published state. The session retains SDK objects, consumes events, and cleans up resources.

BLE-to-Auth adapter

import XTalkBleSdk
import XTalkDeviceAuthSdk

@MainActor
final class XTalkBleAuthTransport: DeviceAuthTransport {
let label = "xTalk BLE"
private(set) var atEncoding: DeviceAuthAtEncoding = .plain
private let ble: XTalkBleClient

init(ble: XTalkBleClient) {
self.ble = ble
}

func setAtEncoding(_ encoding: DeviceAuthAtEncoding) {
atEncoding = encoding
ble.setAtEncoding(encoding == .plain ? .plain : .xor)
}

func runAtCommand(
_ command: String,
timeoutMs: Int,
log: Bool,
responseMode: DeviceAuthResponseMode
) async throws -> [String] {
let mode: XTalkAtResponseMode
switch responseMode {
case .automatic:
mode = .automatic
case .collectFullWindow, .recoverableEncodingProbe:
mode = .collectUntilTimeout
}
return try await ble.runAtCommand(
command,
timeoutMs: timeoutMs,
log: log,
responseMode: mode
)
}
}

Complete session

import Combine
import Foundation
import XTalkBleSdk
import XTalkDeviceAuthSdk

@MainActor
final class XTalkSdkSession: ObservableObject {
@Published private(set) var receivedPayloads: [Data] = []
@Published private(set) var authenticatedIdentity: XTalkDeviceIdentity?
@Published private(set) var lastError: Error?

let ble: XTalkBleClient
let authenticator: XTalkDeviceAuthenticator

private let authTransport: XTalkBleAuthTransport
private var eventTask: Task<Void, Never>?
private var operationTask: Task<Void, Never>?

init() {
let ble = XTalkBleClient()
let transport = XTalkBleAuthTransport(ble: ble)
self.ble = ble
authTransport = transport
authenticator = XTalkDeviceAuthenticator(transport: transport)
startEventConsumption()
}

func startScan() {
lastError = nil
ble.startScan()
}

func connect(_ device: XTalkScanResult) {
cancelOperation()
authenticatedIdentity = nil
ble.stopScan()
ble.connect(id: device.id)
}

func authenticateConnectedDevice() {
cancelOperation()
operationTask = Task { @MainActor [weak self] in
guard let self else { return }
do {
try await waitUntilAtReady(timeout: .seconds(10))
let result = try await authenticator.authenticate()
guard !Task.isCancelled else { return }
authenticatedIdentity = result.identity
ble.setAtEncoding(result.encoding == .plain ? .plain : .xor)
lastError = nil
} catch {
guard !Task.isCancelled else { return }
authenticatedIdentity = nil
lastError = error
}
}
}

func sendBusinessPayload(_ payload: Data) {
guard authenticatedIdentity != nil else {
lastError = XTalkDeviceAuthError.transportUnavailable
return
}
cancelOperation()
operationTask = Task { @MainActor [weak self] in
guard let self else { return }
do {
try await ble.sendRfPayload(payload, timeoutMs: 5_000)
lastError = nil
} catch {
guard !Task.isCancelled else { return }
lastError = error
}
}
}

func queryVersion() {
guard authenticatedIdentity != nil else { return }
cancelOperation()
operationTask = Task { @MainActor [weak self] in
do {
let lines = try await self?.ble.runAtCommand("AT+VER?") ?? []
print(lines)
} catch {
guard !Task.isCancelled else { return }
self?.lastError = error
}
}
}

func disconnect() {
cancelOperation()
authenticatedIdentity = nil
ble.disconnect()
}

func close() {
cancelOperation()
eventTask?.cancel()
eventTask = nil
authenticatedIdentity = nil
ble.teardown()
}

private func cancelOperation() {
operationTask?.cancel()
operationTask = nil
authenticator.cancel()
}

private func waitUntilAtReady(timeout: Duration) async throws {
let deadline = ContinuousClock.now.advanced(by: timeout)
while !ble.atReady {
try Task.checkCancellation()
if case .failed(let error) = ble.connectionState { throw error }
guard ContinuousClock.now < deadline else { throw XTalkBleError.timeout }
try await Task.sleep(for: .milliseconds(50))
}
}

private func startEventConsumption() {
let stream = ble.events
eventTask = Task { @MainActor [weak self] in
for await event in stream {
guard let self, !Task.isCancelled else { break }
switch event {
case .diPayload(let data, _):
receivedPayloads.append(data)
case .connectionStateChanged(.disconnected):
authenticatedIdentity = nil
authenticator.cancel()
case .connectionStateChanged(.failed(let error)):
authenticatedIdentity = nil
authenticator.cancel()
lastError = error
default:
break
}
}
}
}
}

SwiftUI caller

struct DeviceView: View {
@StateObject private var session = XTalkSdkSession()

var body: some View {
List(session.ble.scanResults) { device in
Button("\(device.displayName) \(device.rssi) dBm") {
session.connect(device)
}
}
.toolbar {
Button("Scan") { session.startScan() }
Button("Authenticate") { session.authenticateConnectedDevice() }
}
.onDisappear { session.close() }
}
}

A production app decides when to call close() based on navigation and shared-device ownership. Do not destroy a session in a child view's onDisappear if it is shared across screens.

Acceptance sequence

  1. A fresh install grants Bluetooth access and shows scan results.
  2. Selection reaches .connected, then atReady == true.
  3. Successful authentication displays identity.did; failure cannot enter business flow.
  4. AT queries and RF writes do not interleave responses.
  5. Link loss clears the current authenticated identity; reconnect requires authentication again.
  6. close() ends the event task and SDK transactions.

See the API coverage matrix