Skip to content

iOS SBeacon Framework Beacon Pairing

AigarsM edited this page Feb 2, 2026 · 1 revision

Beacon Pairing

iOS SBeacon Framework v1.0.8

This guide covers pairing SBeacon devices, managing connections, and handling pairing lifecycle.


Overview

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.


Prerequisites


Pairing Workflow

Complete Pairing Flow

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

Basic Pairing

Step-by-Step Pairing

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()
}

Pairing with Timeout Protection

Recommended Implementation

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
    }
}

Checking Connection Status

Query Connected Beacons

// 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")
}

Connection State Properties

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?

Unpairing Beacons

Complete Unpairing

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")
}

Auto-Reconnection

How Auto-Reconnection Works

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.

Monitoring Connection Events

// 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")
}

SwiftUI Example

Complete Pairing View

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
    }
}

UIKit Example

Pairing with Progress Indicator

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
    }
}

Troubleshooting

MAC Address Never Received

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

Pairing Timeout

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)

Beacon Disconnects Immediately

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)

Duplicate Beacons

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
}

Best Practices

  1. Always Set Timeout: Prevent indefinite waiting for MAC address
  2. Clean Up Callbacks: Set to nil after use to prevent memory leaks
  3. Handle Failures: Delete created beacon if pairing fails
  4. User Feedback: Show progress indicator during pairing
  5. Proximity: Instruct user to be close to beacon during pairing (< 3 meters)
  6. Battery Check: Warn if beacon signal is weak (might indicate low battery)
  7. Duplicate Prevention: Check if beacon already exists before pairing
  8. Background Handling: Don't attempt pairing when app is in background

Related Pages

Clone this wiki locally