Інтеграція iBeacon для визначення proximity в iOS-приложенні
iBeacon — це профіль поверх Bluetooth LE, де маяк постійно рассилає Advertisement Packet з UUID, major та minor. Телефон отримує цей пакет та на основі RSSI розраховує «близість»: CLProximity.immediate (до 0.5 м), near, far, unknown. Розробники часто очікують GPS-точність. Реальність інша: RSSI стрибає ±15 dBm навіть у вакуумі, у торговому залі з металевими стелажами — ±25 dBm. CLProximity.immediate легко може бути метра три.
Де частіше ломаються проблеми
CLLocationManager та дозволи
Починаючи з iOS 13, для ranging маяків потрібна дозвіл whenInUse — це добра новина. Погана: якщо користувач дав тільки WhenInUse, то моніторинг регіонів (startMonitoring(for:)) в фоне працює, але startRangingBeacons() — ні. Приложення мовчазно не отримує ніяких сповіщень про близість.
Типова помилка — запитування дозволу в viewDidLoad без пояснення контексту. Apple з iOS 14 при повторному запиті Always просто ігнорує виклик, якщо користувач вже відповів «При використанні». Потрібно вести користувача в Settings через UIApplication.openSettingsURLString та пояснювати, чому приложенню потрібен фоновий доступ.
Обмеження 20 регіонів та batch-ranging
CLLocationManager дозволяє одночасно моніторити не більш 20 регіонів (будь-яких — geofence + iBeacon разом). Якщо у магазині 50 відділів, кожний зі своїм beacon UUID — схема не працює прямо. Рішення: один UUID на все закладу, major — зона, minor — конкретна точка. При входе в регіон (по UUID) включаємо ranging та вже в ньому розбираємо major/minor.
Ranging активний тільки коли приложення на передньому плані або є активний CLBeaconRegion у моніторингу. У фоне приходять тільки события didEnterRegion / didExitRegion — не постійний потік RSSI.
Як ми будуємо інтеграцію
Архітектура CLLocationManager + Combine
Виносимо всю роботу з CoreLocation в BeaconScanner — окремий сервіс, ізольований від UI. Він публікує AnyPublisher<[CLBeacon], Never>, UI підписується через SwiftUI onReceive або @Published у 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-фільтрацію робимо через скользяче середнє по останнім 5 значеннях — це забирає випадкові выбросы без помітної затримки.
Калібрування txPower
Значення accuracy в CLBeacon розраховується по формулі з поправкою на txPower з пакета маяка. Якщо маяк налаштований з дефолтним txPower = -59 dBm, але реально радіює -65 dBm (через корпус або батарейку), розрахункова дистанція буде занижена на 30–40%. Для точних сценаріїв (навігація у музеї, point-of-sale) — калібруємо кожний маяк на місці установки з урахуванням навколишнього середовища.
Типові помилки при розгорітанні
- Маяки з одинаковим UUID та major/minor —
CLBeacon.accuracyбереться від найближчого, але iOS може плутатися та повертати одного з дублів з застарілим RSSI - Занадто високий advertising interval на маяках (>1000 мс) —
didRangeвикликається раз на секунду,accuracyреагує з затримкою 3–5 с - Металеві стелажи, дзеркала, акваріуми — BLE відбивається та створює мертві зони; потребує тестування на реальному об'єкті
Терміни
Базова інтеграція ranging + моніторинг регіонів — 4–6 робочих днів. Якщо потрібна навігація всередину приміщень з картою або інтеграція з серверною аналітикою — від 3 тижнів. Оцінка після вивчення кількості маяків та сценаріїв використання.







