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:nilon success,CalibrationErroron 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:
tripCount- Number of unsynced trip recordsincidentCount- Number of incident recordsdbEventCount- 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
TripRecordobjects (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
DrivingBehaviorEventobjects (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
IncidentRecordobjects 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:
RealTimeDrivingBehaviorEventwith 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:
DrivingBehaviorEnableFlagsobject 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
- Always Use [weak self]: Prevent retain cycles in closures
- Clean Up on Deinit: Set callbacks to
nilwhen done - Single Responsibility: Don't do too much in callbacks - delegate to methods
- Error Handling: Check for
nilor error states before proceeding - Timeout Protection: Use timers for callbacks that may not fire (pairing, calibration)
- Combine with Notifications: Use NotificationCenter for global events
Related Pages
- Trip Management - Trip callbacks
- Driving Behavior - DB event callbacks
- Beacon Pairing - MAC address callback
- Calibration - Calibration callback