iOS SBeacon Framework Event Handling - scopetech/Mobile-Public GitHub Wiki

Event Handling

iOS SBeacon Framework v1.0.8

This guide covers the callback-based event system for receiving data and notifications from SBeacon devices.


Overview

The iOS SBeacon Framework uses closure-based callbacks for asynchronous event delivery. All callbacks are optional properties on SBeaconManager.shared that you set to receive notifications.


Callback Architecture

Setting Callbacks

// Set callback closure
SBeaconManager.shared.tripStatusChanged = { status in
    print("Trip status: \(status)")
}

// Clear callback (important for cleanup)
SBeaconManager.shared.tripStatusChanged = nil

Memory Management

ALWAYS use [weak self] to prevent retain cycles:

// ✅ CORRECT
SBeaconManager.shared.tripStatusChanged = { [weak self] status in
    guard let self = self else { return }
    self.handleTripStatus(status)
}

// ❌ INCORRECT - Creates retain cycle
SBeaconManager.shared.tripStatusChanged = { status in
    self.handleTripStatus(status)  // Captures self strongly
}

Complete Callback Reference

1. macAddressReceived

Type: ((String) -> Void)?

When Called: After pairing, when beacon's full MAC address is retrieved

Usage:

SBeaconManager.shared.macAddressReceived = { [weak self] macAddress in
    print("MAC address: \(macAddress)")
    // Update beacon object, save to database, etc.
}

Best Practice: Set timeout protection (15 seconds) when pairing


2. tripStatusChanged

Type: ((TripStatus) -> Void)?

When Called: Whenever trip status changes on connected "car beacon"

Possible Values:

  • .Idle - No trip in progress
  • .Start - Trip starting
  • .Active - Trip ongoing
  • .End - Trip ending
  • .Timeout - Trip timed out

Usage:

SBeaconManager.shared.tripStatusChanged = { [weak self] status in
    switch status {
    case .Start:
        self?.onTripStart()
    case .End:
        self?.onTripEnd()
    default:
        break
    }
}

3. calibrationComplete

Type: ((CalibrationError?) -> Void)?

When Called: After calibration process finishes

Parameters:

  • error: nil on success, CalibrationError on failure

Usage:

SBeaconManager.shared.calibrationComplete = { [weak self] error in
    if let error = error {
        self?.showError("Calibration failed: \(error)")
    } else {
        self?.showSuccess("Calibration complete")
    }
}

4. tripInformationReceived

Type: ((Int, Int, Int) -> Void)?

When Called: After calling requestTripInformation()

Parameters:

  1. tripCount - Number of unsynced trip records
  2. incidentCount - Number of incident records
  3. dbEventCount - Number of driving behavior events

Usage:

SBeaconManager.shared.tripInformationReceived = { [weak self] tripCount, incidentCount, dbEventCount in
    print("Unsynced: \(tripCount) trips, \(incidentCount) incidents, \(dbEventCount) DB events")

    if tripCount > 0 {
        self?.requestTripRecords()
    }
}

5. tripRecordsReceived

Type: (([TripRecord]) -> Void)?

When Called: After calling requestTripRecords()

Parameters:

  • Array of TripRecord objects (9 bytes each)

Usage:

SBeaconManager.shared.tripRecordsReceived = { [weak self] records in
    print("Received \(records.count) trip records")

    for record in records {
        guard let startDate = record.startDate,
              let endDate = record.endDate else { continue }

        print("Trip \(record.index): \(startDate) to \(endDate)")
        self?.uploadTripRecord(record)
    }
}

6. drivingBehaviorEventsReceived

Type: (([DrivingBehaviorEvent]) -> Void)?

When Called: After calling requestDrivingBehaviorEvents()

Parameters:

  • Array of DrivingBehaviorEvent objects (18 bytes each, max 80 events)

Usage:

SBeaconManager.shared.drivingBehaviorEventsReceived = { [weak self] events in
    print("Received \(events.count) driving behavior events")

    for event in events {
        print("Event \(event.index): \(event.eventType) - \(event.maxMagnitude)G")
        self?.uploadDrivingBehaviorEvent(event)
    }

    // Clear events after upload
    if !events.isEmpty {
        self?.clearAllDrivingBehaviorEvents()
    }
}

7. incidentRecordsReceived

Type: (([IncidentRecord]) -> Void)?

When Called: After incident data is retrieved from beacon

Parameters:

  • Array of IncidentRecord objects with accelerometer samples

Usage:

SBeaconManager.shared.incidentRecordsReceived = { [weak self] incidents in
    print("Received \(incidents.count) incident records")

    for incident in incidents {
        print("Incident: max vector = \(incident.maximumResultantVector)")
        self?.uploadIncident(incident)
    }
}

8. realTimeDrivingBehaviorEventDetected

Type: ((RealTimeDrivingBehaviorEvent) -> Void)?

When Called: Instantly when harsh driving event detected during trip

Parameters:

  • RealTimeDrivingBehaviorEvent with event type, axis, and flags

Usage:

SBeaconManager.shared.realTimeDrivingBehaviorEventDetected = { [weak self] event in
    // Show immediate feedback
    self?.showAlert(
        title: "Harsh Driving Detected",
        message: "\(event.eventType.description) on \(event.axis.description)"
    )

    // Play sound, haptic feedback, etc.
    self?.playWarningSound()
}

9. drivingBehaviorEnableFlagsReceived

Type: ((DrivingBehaviorEnableFlags) -> Void)?

When Called: After calling readDrivingBehaviorEnableFlags()

Parameters:

  • DrivingBehaviorEnableFlags object with boolean flags

Usage:

SBeaconManager.shared.drivingBehaviorEnableFlagsReceived = { [weak self] enableFlags in
    self?.isCorneringEnabled = enableFlags.corneringEnabled
    self?.isBrakingEnabled = enableFlags.brakingEnabled
    self?.isAccelerationEnabled = enableFlags.accelerationEnabled
}

Callback Lifecycle Management

Setting Up Callbacks

class TripMonitor {
    init() {
        setupCallbacks()
    }

    private func setupCallbacks() {
        SBeaconManager.shared.tripStatusChanged = { [weak self] status in
            self?.handleTripStatus(status)
        }

        SBeaconManager.shared.tripRecordsReceived = { [weak self] records in
            self?.handleTripRecords(records)
        }
    }

    deinit {
        // CRITICAL: Clean up callbacks
        SBeaconManager.shared.tripStatusChanged = nil
        SBeaconManager.shared.tripRecordsReceived = nil
    }
}

NotificationCenter Integration

Using NotificationCenter

Framework also posts NotificationCenter notifications for key events:

// Beacon connected
NotificationCenter.default.addObserver(
    forName: SBeaconManager.beaconConnectedNotification,
    object: nil,
    queue: .main
) { notification in
    print("Beacon connected")
}

// Beacon disconnected
NotificationCenter.default.addObserver(
    forName: SBeaconManager.beaconDisconnectedNotification,
    object: nil,
    queue: .main
) { notification in
    print("Beacon disconnected")
}

// Battery status changed
NotificationCenter.default.addObserver(
    forName: SBeaconManager.batteryStatusChangedNotification,
    object: nil,
    queue: .main
) { notification in
    print("Battery status changed")
}

// Beacon range changed
NotificationCenter.default.addObserver(
    forName: SBeaconManager.beaconInRangeNotification,
    object: nil,
    queue: .main
) { notification in
    print("Beacon in range")
}

NotificationCenter.default.addObserver(
    forName: SBeaconManager.beaconOutOfRangeNotification,
    object: nil,
    queue: .main
) { notification in
    print("Beacon out of range")
}

// Beacon paired
NotificationCenter.default.addObserver(
    forName: SBeaconManager.beaconPairedNotification,
    object: nil,
    queue: .main
) { notification in
    print("Beacon paired")
}

// Beacon unpaired
NotificationCenter.default.addObserver(
    forName: SBeaconManager.beaconUnpairedNotification,
    object: nil,
    queue: .main
) { notification in
    print("Beacon unpaired")
}

Complete Integration Example

Multi-Screen App with Proper Lifecycle

import Foundation
import Combine
import BLEFramework

class BeaconEventCoordinator: ObservableObject {
    @Published var currentTripStatus: TripStatus = .Idle
    @Published var unpairedBeacons: [UnpairedSBeacon] = []
    @Published var tripRecords: [TripRecord] = []
    @Published var drivingBehaviorEvents: [DrivingBehaviorEvent] = []

    private var cancellables = Set<AnyCancellable>()

    init() {
        setupAllCallbacks()
        setupNotificationObservers()
    }

    private func setupAllCallbacks() {
        // Trip monitoring
        SBeaconManager.shared.tripStatusChanged = { [weak self] status in
            self?.currentTripStatus = status

            if status == .End {
                self?.requestTripData()
            }
        }

        // Trip data
        SBeaconManager.shared.tripRecordsReceived = { [weak self] records in
            self?.tripRecords = records
        }

        // Driving behavior
        SBeaconManager.shared.drivingBehaviorEventsReceived = { [weak self] events in
            self?.drivingBehaviorEvents = events
        }

        // Real-time events
        SBeaconManager.shared.realTimeDrivingBehaviorEventDetected = { [weak self] event in
            self?.handleRealTimeEvent(event)
        }
    }

    private func setupNotificationObservers() {
        NotificationCenter.default.publisher(for: SBeaconManager.beaconConnectedNotification)
            .sink { [weak self] _ in
                self?.onBeaconConnected()
            }
            .store(in: &cancellables)

        NotificationCenter.default.publisher(for: SBeaconManager.beaconDisconnectedNotification)
            .sink { [weak self] _ in
                self?.onBeaconDisconnected()
            }
            .store(in: &cancellables)
    }

    private func requestTripData() {
        guard let beacon = getConnectedBeacon() else { return }
        SBeaconManager.shared.requestTripRecords(beacon: beacon)
        SBeaconManager.shared.requestDrivingBehaviorEvents(beacon: beacon)
    }

    private func handleRealTimeEvent(_ event: RealTimeDrivingBehaviorEvent) {
        // Show notification, play sound, etc.
    }

    private func onBeaconConnected() {
        // Refresh UI, request data, etc.
    }

    private func onBeaconDisconnected() {
        // Update UI state
        currentTripStatus = .Unknown
    }

    private func getConnectedBeacon() -> SCPBeacon? {
        return SBeaconManager.shared.getConnectedBeacons().first
    }

    deinit {
        // Clean up all callbacks
        SBeaconManager.shared.tripStatusChanged = nil
        SBeaconManager.shared.tripRecordsReceived = nil
        SBeaconManager.shared.drivingBehaviorEventsReceived = nil
        SBeaconManager.shared.realTimeDrivingBehaviorEventDetected = nil
    }
}

Best Practices

  1. Always Use [weak self]: Prevent retain cycles in closures
  2. Clean Up on Deinit: Set callbacks to nil when done
  3. Single Responsibility: Don't do too much in callbacks - delegate to methods
  4. Error Handling: Check for nil or error states before proceeding
  5. Timeout Protection: Use timers for callbacks that may not fire (pairing, calibration)
  6. Combine with Notifications: Use NotificationCenter for global events

Related Pages