Repository navigation
iOS SBeacon Framework Beacon Pairing
iOS SBeacon Framework v1.0.8
This guide covers pairing SBeacon devices, managing connections, and handling pairing lifecycle.
Pairing associates an unpaired SBeacon device with your user account, stores it in CoreData, and establishes a persistent BLE connection. Once paired, the beacon automatically reconnects whenever it's in range.
- Beacon Scanning to discover unpaired beacons
- Initialization completed
1. Discover beacon via scanning → UnpairedSBeacon
2. Create SCPBeacon in CoreData
3. Call beaconPaired() to initiate BLE connection
4. Wait for MAC address callback (up to 15 seconds)
5. Beacon is fully paired and persisted
import BLEFramework
func pairBeacon(_ unpairedBeacon: UnpairedSBeacon) {
// Step 1: Create beacon in CoreData
guard let savedBeacon = SCPBeaconManager.shared.createBeacon(
name: "", // Optional display name
proximityUUID: unpairedBeacon.uuid.uuidString,
major: unpairedBeacon.major,
minor: unpairedBeacon.minor,
activities: nil,
location: nil,
subscriptionId: 0,
subscriptionUnitSerialNumber: nil,
range: unpairedBeacon.range,
macAddress: unpairedBeacon.macAddressEnding
) else {
print("Failed to create beacon")
return
}
// Step 2: Set MAC address callback
SBeaconManager.shared.macAddressReceived = { [weak self] macAddress in
print("MAC address received: \(macAddress)")
// Update beacon with full MAC address
savedBeacon.macAddress = macAddress
BeaconCoreDataManager.shared.saveContext()
// Clean up callback
SBeaconManager.shared.macAddressReceived = nil
// Pairing complete!
self?.onPairingComplete(savedBeacon)
}
// Step 3: Initiate pairing and connection
SBeaconManager.shared.beaconPaired(beacon: unpairedBeacon)
// Step 4: Start scan to connect
SBeaconManager.shared.scanForBeacons()
}import Foundation
import BLEFramework
class BeaconPairingManager {
private var pairingTimer: Timer?
private let pairingTimeout: TimeInterval = 15.0
func pairBeacon(
_ unpairedBeacon: UnpairedSBeacon,
completion: @escaping (Result<SCPBeacon, PairingError>) -> Void
) {
// Create beacon in CoreData
guard let savedBeacon = SCPBeaconManager.shared.createBeacon(
name: "",
proximityUUID: unpairedBeacon.uuid.uuidString,
major: unpairedBeacon.major,
minor: unpairedBeacon.minor,
activities: nil,
location: nil,
subscriptionId: 0,
subscriptionUnitSerialNumber: nil,
range: unpairedBeacon.range,
macAddress: unpairedBeacon.macAddressEnding
) else {
completion(.failure(.creationFailed))
return
}
// Set MAC address callback
SBeaconManager.shared.macAddressReceived = { [weak self] macAddress in
guard let self = self else { return }
// Cancel timeout timer
self.pairingTimer?.invalidate()
self.pairingTimer = nil
// Update beacon
savedBeacon.macAddress = macAddress
BeaconCoreDataManager.shared.saveContext()
// Clean up callback
SBeaconManager.shared.macAddressReceived = nil
completion(.success(savedBeacon))
}
// Start timeout timer
pairingTimer = Timer.scheduledTimer(withTimeInterval: pairingTimeout, repeats: false) { [weak self] _ in
guard let self = self else { return }
// Timeout reached
SBeaconManager.shared.macAddressReceived = nil
// Delete failed beacon
SCPBeaconManager.shared.deleteBeacon(id: savedBeacon.objectID)
completion(.failure(.timeout))
}
// Initiate pairing
SBeaconManager.shared.beaconPaired(beacon: unpairedBeacon)
SBeaconManager.shared.scanForBeacons()
}
enum PairingError: Error {
case creationFailed
case timeout
case alreadyPaired
}
}// Get all connected beacons
let connectedBeacons = SBeaconManager.shared.getConnectedBeacons()
for beacon in connectedBeacons {
print("Beacon \(beacon.macAddress ?? "") is connected")
}
// Check specific beacon
func isBeaconConnected(_ beacon: SCPBeacon) -> Bool {
return SBeaconManager.shared.beaconIsConnected(beacon: beacon)
}
// Check beacon range status (also indicates connection)
if beacon.isInRange {
print("Beacon is in range and connected")
}let beacon: SCPBeacon = ...
// Is beacon currently connected?
let isConnected = beacon.isInRange // Boolean
// Signal strength (RSSI)
let rssi = beacon.range // NSNumber? (-100 to 0)
// Last seen timestamp
let lastSeen = beacon.lastSeenDateInternalUsageOnly // Date?
// BLE peripheral UUID
let peripheralUUID = beacon.address // UUID?func unpairBeacon(_ beacon: SCPBeacon) {
// 1. Disconnect BLE connection
SBeaconManager.shared.beaconUnpaired(beacon: beacon)
// 2. Remove from activities (if associated)
if let activities = beacon.activities as? Set<SCPActivity> {
for activity in activities {
activity.removeFromBeacons(beacon)
}
}
// 3. Delete from CoreData
SCPBeaconManager.shared.deleteBeacon(id: beacon.objectID)
print("Beacon unpaired successfully")
}Once paired, beacons automatically reconnect when:
- App launches (foreground or background)
- Beacon comes into range
- Bluetooth is re-enabled
- Connection is lost and beacon is still in range
Framework Behavior:
1. ConnectionTimer runs every 45 seconds
2. Checks all paired beacons (from CoreData)
3. Retrieves peripherals by UUID
4. Connects to disconnected beacons
5. Re-establishes service discovery
No action required - auto-reconnection is automatic.
// Listen for connection notifications
NotificationCenter.default.addObserver(
forName: SBeaconManager.beaconConnectedNotification,
object: nil,
queue: .main
) { notification in
print("A beacon connected")
// Refresh UI
}
NotificationCenter.default.addObserver(
forName: SBeaconManager.beaconDisconnectedNotification,
object: nil,
queue: .main
) { notification in
print("A beacon disconnected")
// Update UI
}
// Range change notifications
NotificationCenter.default.addObserver(
forName: SBeaconManager.beaconInRangeNotification,
object: nil,
queue: .main
) { notification in
print("Beacon came into range")
}
NotificationCenter.default.addObserver(
forName: SBeaconManager.beaconOutOfRangeNotification,
object: nil,
queue: .main
) { notification in
print("Beacon went out of range")
}import SwiftUI
import BLEFramework
struct BeaconPairingView: View {
@StateObject private var viewModel: BeaconPairingViewModel
let unpairedBeacon: UnpairedSBeacon
init(unpairedBeacon: UnpairedSBeacon) {
self.unpairedBeacon = unpairedBeacon
_viewModel = StateObject(wrappedValue: BeaconPairingViewModel(beacon: unpairedBeacon))
}
var body: some View {
VStack(spacing: 20) {
if viewModel.isPairing {
ProgressView("Pairing beacon...")
Text("Please wait up to 15 seconds")
.font(.caption)
.foregroundColor(.secondary)
} else if let error = viewModel.error {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundColor(.red)
Text(error)
.foregroundColor(.red)
Button("Retry") {
viewModel.startPairing()
}
.buttonStyle(.bordered)
} else if viewModel.pairingComplete {
Image(systemName: "checkmark.circle")
.font(.largeTitle)
.foregroundColor(.green)
Text("Beacon paired successfully!")
} else {
Text("****\(unpairedBeacon.macAddressEnding)")
.font(.headline)
Button("Pair Beacon") {
viewModel.startPairing()
}
.buttonStyle(.borderedProminent)
}
}
.padding()
.navigationTitle("Pair Beacon")
}
}
class BeaconPairingViewModel: ObservableObject {
@Published var isPairing = false
@Published var pairingComplete = false
@Published var error: String?
private let beacon: UnpairedSBeacon
private var pairingTimer: Timer?
private var savedBeacon: SCPBeacon?
init(beacon: UnpairedSBeacon) {
self.beacon = beacon
}
func startPairing() {
isPairing = true
error = nil
pairingComplete = false
// Create beacon
guard let savedBeacon = SCPBeaconManager.shared.createBeacon(
name: "",
proximityUUID: beacon.uuid.uuidString,
major: beacon.major,
minor: beacon.minor,
activities: nil,
location: nil,
subscriptionId: 0,
subscriptionUnitSerialNumber: nil,
range: beacon.range,
macAddress: beacon.macAddressEnding
) else {
self.error = "Failed to create beacon"
self.isPairing = false
return
}
self.savedBeacon = savedBeacon
// Set callback
SBeaconManager.shared.macAddressReceived = { [weak self] macAddress in
guard let self = self else { return }
self.pairingTimer?.invalidate()
savedBeacon.macAddress = macAddress
BeaconCoreDataManager.shared.saveContext()
SBeaconManager.shared.macAddressReceived = nil
self.isPairing = false
self.pairingComplete = true
}
// Start timeout
pairingTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
guard let self = self else { return }
SBeaconManager.shared.macAddressReceived = nil
if let savedBeacon = self.savedBeacon {
SCPBeaconManager.shared.deleteBeacon(id: savedBeacon.objectID)
}
self.isPairing = false
self.error = "Pairing timeout. Please try again."
}
// Initiate pairing
SBeaconManager.shared.beaconPaired(beacon: beacon)
SBeaconManager.shared.scanForBeacons()
}
deinit {
pairingTimer?.invalidate()
SBeaconManager.shared.macAddressReceived = nil
}
}import UIKit
import BLEFramework
class BeaconPairingViewController: UIViewController {
private let unpairedBeacon: UnpairedSBeacon
private var pairingTimer: Timer?
private var savedBeacon: SCPBeacon?
private let activityIndicator = UIActivityIndicatorView(style: .large)
private let statusLabel = UILabel()
init(beacon: UnpairedSBeacon) {
self.unpairedBeacon = beacon
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
startPairing()
}
private func setupUI() {
view.backgroundColor = .systemBackground
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
statusLabel.translatesAutoresizingMaskIntoConstraints = false
statusLabel.text = "Pairing beacon..."
statusLabel.textAlignment = .center
view.addSubview(activityIndicator)
view.addSubview(statusLabel)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
statusLabel.topAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 20),
statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
activityIndicator.startAnimating()
}
private func startPairing() {
guard let savedBeacon = SCPBeaconManager.shared.createBeacon(
name: "",
proximityUUID: unpairedBeacon.uuid.uuidString,
major: unpairedBeacon.major,
minor: unpairedBeacon.minor,
activities: nil,
location: nil,
subscriptionId: 0,
subscriptionUnitSerialNumber: nil,
range: unpairedBeacon.range,
macAddress: unpairedBeacon.macAddressEnding
) else {
showError("Failed to create beacon")
return
}
self.savedBeacon = savedBeacon
SBeaconManager.shared.macAddressReceived = { [weak self] macAddress in
self?.onPairingSuccess(macAddress: macAddress)
}
pairingTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
self?.onPairingTimeout()
}
SBeaconManager.shared.beaconPaired(beacon: unpairedBeacon)
SBeaconManager.shared.scanForBeacons()
}
private func onPairingSuccess(macAddress: String) {
pairingTimer?.invalidate()
savedBeacon?.macAddress = macAddress
BeaconCoreDataManager.shared.saveContext()
SBeaconManager.shared.macAddressReceived = nil
activityIndicator.stopAnimating()
statusLabel.text = "Beacon paired successfully!"
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
}
private func onPairingTimeout() {
SBeaconManager.shared.macAddressReceived = nil
if let savedBeacon = savedBeacon {
SCPBeaconManager.shared.deleteBeacon(id: savedBeacon.objectID)
}
showError("Pairing timeout. Please try again.")
}
private func showError(_ message: String) {
activityIndicator.stopAnimating()
statusLabel.text = message
let alert = UIAlertController(title: "Pairing Failed", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
})
present(alert, animated: true)
}
deinit {
pairingTimer?.invalidate()
SBeaconManager.shared.macAddressReceived = nil
}
}Problem: macAddressReceived callback never fires
Solutions:
- Ensure beacon is powered on and in range (< 5 meters during pairing)
- Check Bluetooth is enabled
- Verify beacon isn't already paired to another account
- Try power cycling the beacon (remove battery, wait 10s, reinstall)
- Increase timeout to 20-30 seconds for initial pairing
Problem: Pairing times out after 15 seconds
Causes:
- Beacon is out of range
- Beacon battery is low
- BLE interference from other devices
- Beacon firmware issue
Solutions:
- Move closer to beacon (< 3 meters)
- Check beacon battery level
- Retry pairing in location with less RF interference
- Check beacon is advertising (should blink LED)
Problem: Beacon connects then disconnects
Solutions:
- Check beacon battery isn't critically low
- Verify CoreData saved properly
- Ensure app has background BLE permission
- Check for multiple BLE managers (use singleton)
Problem: Same beacon appears multiple times in paired list
Prevention:
// Check if beacon already exists before pairing
func beaconExists(minor: NSNumber, major: NSNumber) -> Bool {
let beacons = SCPBeaconManager.shared.getAllBeacons(exceptCar: false) ?? []
return beacons.contains { $0.minor == minor && $0.major == major }
}
// Before pairing
if beaconExists(minor: unpairedBeacon.minor, major: unpairedBeacon.major) {
print("Beacon already paired")
return
}- Always Set Timeout: Prevent indefinite waiting for MAC address
-
Clean Up Callbacks: Set to
nilafter use to prevent memory leaks - Handle Failures: Delete created beacon if pairing fails
- User Feedback: Show progress indicator during pairing
- Proximity: Instruct user to be close to beacon during pairing (< 3 meters)
- Battery Check: Warn if beacon signal is weak (might indicate low battery)
- Duplicate Prevention: Check if beacon already exists before pairing
- Background Handling: Don't attempt pairing when app is in background
- Beacon Scanning - Discover beacons before pairing
- Trip Management - Use paired beacons for trips
- Event Handling - Complete callback reference
- Data Models - SCPBeacon structure
Consolidated SDK
SmartDrive
PortalApiBase
PortalApiClient
Wellness
SBeacon
- Overview
- Installation
- Initialization
- Beacon Scanning
- Beacon Pairing
- Trip Management
- Driving Behavior
- Calibration
- Event Handling
- Data Models
- API Reference
Consolidated SPM
SDApiManager
CCFrameworkOpen
SmartDrive
SBeacon