Интеграция 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 недель. Оценка после изучения количества маяков и сценариев использования.







