iBeacon Integration for Proximity Detection in iOS Application
iBeacon is a profile over Bluetooth LE where beacon continuously broadcasts Advertisement Packet with UUID, major and minor. Phone receives this packet and based on RSSI calculates "proximity": CLProximity.immediate (up to 0.5 m), near, far, unknown. Developers often expect GPS accuracy. Reality different: RSSI fluctuates ±15 dBm even in vacuum, in retail with metal shelves — ±25 dBm. CLProximity.immediate easily can be three meters.
Where Problems Most Often Occur
CLLocationManager and Permissions
Starting with iOS 13, ranging beacons requires whenInUse permission — good news. Bad news: if user granted only WhenInUse, then region monitoring (startMonitoring(for:)) works in background, but startRangingBeacons() — no. App silently gets no proximity notifications.
Typical error — requesting permission in viewDidLoad without context. Apple since iOS 14 ignores Always request if user already answered "When in Use". Must guide user to Settings via UIApplication.openSettingsURLString and explain why background access needed.
20-Region Limit and Batch Ranging
CLLocationManager allows monitoring max 20 regions simultaneously (any — geofence + iBeacon together). If store has 50 departments, each with own beacon UUID — scheme doesn't work directly. Solution: one UUID for entire facility, major — zone, minor — specific point. On entering region (by UUID) enable ranging and parse major/minor within it.
Ranging active only when app foreground or has active CLBeaconRegion in monitoring. In background only didEnterRegion / didExitRegion events — no constant RSSI stream.
How We Build Integration
CLLocationManager + Combine Architecture
Extract all CoreLocation work to BeaconScanner — separate service isolated from UI. It publishes AnyPublisher<[CLBeacon], Never>, UI subscribes via SwiftUI onReceive or @Published in ViewModel.
final class BeaconScanner: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private let beaconsSubject = PassthroughSubject<[CLBeacon], Never>()
var beaconsPublisher: AnyPublisher<[CLBeacon], Never> {
beaconsSubject.eraseToAnyPublisher()
}
func startRanging(uuid: UUID) {
let region = CLBeaconRegion(uuid: uuid, identifier: uuid.uuidString)
locationManager.startMonitoring(for: region)
locationManager.startRangingBeacons(satisfying: region.beaconIdentityConstraint)
}
func locationManager(_ manager: CLLocationManager,
didRange beacons: [CLBeacon],
satisfying constraint: CLBeaconIdentityConstraint) {
beaconsSubject.send(beacons)
}
}
RSSI filtering via moving average over last 5 values — removes random outliers without noticeable latency.
txPower Calibration
accuracy value in CLBeacon calculated by formula with txPower adjustment from beacon packet. If beacon configured with default txPower = -59 dBm but actually broadcasts -65 dBm (due to case or battery), calculated distance underestimated by 30–40%. For precise scenarios (museum navigation, point-of-sale) — calibrate each beacon on-site considering environment.
Typical Deployment Errors
- Beacons with identical UUID, major/minor —
CLBeacon.accuracyfrom nearest, but iOS can confuse and return one of duplicates with stale RSSI - Too high advertising interval on beacons (>1000 ms) —
didRangecalled once per second,accuracyresponds with 3–5 s delay - Metal shelves, mirrors, aquariums — BLE reflects and creates dead zones; requires testing on real object
Timeframes
Basic integration ranging + region monitoring — 4–6 work days. With indoor navigation map or server analytics integration — from 3 weeks. Estimate after studying beacon count and use scenarios.







