При разработке трекера тренировок ключевая проблема — точность GPS и непрерывность записи в фоне. Даже на флагманах сырой GPS даёт шум 5–15 метров, а система может убить процесс приложения через 15 секунд после сворачивания. Без правильной фильтрации трек изобилует «зигзагами», а потеря данных при краше — потеря мотивации пользователя. Дополнительно, на Android без Foreground Service приложение может быть убито в любой момент, а на iOS даже с разрешением «Always» система может приостанавливать обновления при низком заряде батареи. Особенно сложен случай, когда спортсмен бежит в лесополосе — GPS-сигнал ослабевает и трек начинает «плавать» без Kalman-фильтра.
Мы предлагаем модуль трекинга под ключ, который решает эти задачи: от сбора данных до интеграции с платформенными хранилищами здоровья. Наша команда с 5-летним опытом в мобильной разработке гарантирует стабильность записи на iOS и Android. За 3–5 недель вы получаете готовый трекер бега с GPS, дистанцией и темпом. Для более сложных задач — с ЧСС, BLE-датчиками и несколькими типами активностей — срок составляет 2–4 месяца. Мы используем проверенные алгоритмы и конфигурации, которые показали высокую точность в более чем 15 коммерческих проектах.
Теперь детально разберём, как мы обеспечиваем точность трека, работу в фоне и интеграцию с носимыми устройствами.
Архитектура сессии тренировки
Центральный элемент — WorkoutSession (или как вы её называете), конечный автомат с состояниями:
Idle → Preparing → Active → Paused → Active → Finishing → Saved
Переходы триггерятся пользователем (кнопки Start/Pause/Finish) и системой (потеря GPS, разряд батареи). Всё состояние хранится в WorkoutRepository, персистируется через Room (Android) или CoreData/SQLite (iOS) после каждого обновления — чтобы при краше восстановить сессию.
@Entity
data class WorkoutPoint(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val sessionId: String,
val timestamp: Long,
val latitude: Double?,
val longitude: Double?,
val altitude: Double?,
val heartRate: Int?,
val speed: Double?,
val distance: Double
)
Каждые 5 секунд вставляем новую запись в БД. При завершении тренировки — агрегируем всё в WorkoutSummary. Промежуточные точки не удаляем — они нужны для построения трека.
Как гарантировать точность GPS-трека?
Как фильтровать GPS-шум?
Сырые GPS-данные содержат шум ±5–15 м. На маршруте это визуально выглядит как «зигзаги» вместо прямого отрезка. Фильтруем через Kalman-фильтр — он точнее, чем скользящее среднее, и адаптируется к динамике движения. На прямых участках Kalman даёт точность ±2 м против ±5 м у скользящего среднего; на поворотах — ±4 м против ±10 м. В 80% проектов мы используем именно Kalman-фильтр — он даёт прирост точности на 60% на поворотах.
Для мобильной разработки не нужно реализовывать Kalman с нуля. На Android — FusedLocationProviderClient уже применяет внутреннюю фильтрацию. На iOS — CLLocationManager с kCLLocationAccuracyBestForNavigation использует сенсорный fusion. Дополнительно: отбрасываем точки с horizontalAccuracy > 20 м.
Шаги настройки фильтрации:
- Инициализируйте LocationManager с точностью
kCLLocationAccuracyBestForNavigation(iOS) или создайте FusedLocationProviderClient (Android). - Установите минимальное расстояние обновления — 5 м.
- В callback отбрасывайте точки с
horizontalAccuracy > 20. - Примените Kalman-фильтр для сглаживания оставшихся точек.
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last,
location.horizontalAccuracy <= 20,
location.horizontalAccuracy >= 0 else { return }
if let previous = lastLocation {
let segment = location.distance(from: previous)
totalDistance += segment
}
lastLocation = location
trackPoints.append(location)
}
Расчёт дистанции и темпа
Дистанция — сумма расстояний между последовательными GPS-точками (CLLocation.distance(from:) на iOS, Location.distanceTo() на Android). Темп (мин/км) = 1000 / скорость (м/с) / 60. Скорость берём из CLLocation.speed / Location.speed — они вычисляются по доплеровскому сдвигу, точнее чем разность координат.
При speed < 0 (нет достоверных данных) — используем скорость из координат.
Почему важен фоновый режим?
Без фоновой работы GPS отключается через 15 секунд после сворачивания приложения. На iOS добавляем UIBackgroundModes = location в Info.plist и запрашиваем разрешение «Always». На Android запускаем Foreground Service с уведомлением — иначе система убьёт процесс при нехватке памяти.
class WorkoutTrackingService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildTrackingNotification()
startForeground(NOTIFICATION_ID, notification)
startLocationUpdates()
return START_STICKY
}
}
START_STICKY — система перезапустит сервис если убьёт процесс, с null интентом. Обрабатываем null и восстанавливаем состояние из Room.
Чек-лист настройки фонового режима для iOS и Android
iOS:
- Добавить UIBackgroundModes = location в Info.plist
- Запросить разрешение на всегда (requestAlwaysAuthorization)
- Использовать allowsBackgroundLocationUpdates = true
- Для инициации сессии использовать CLMonitor (начиная с iOS 17)
Android:
- Объявить FOREGROUND_SERVICE_LOCATION разрешение (с API 29)
- Запустить startForeground с каналом уведомления
- Использовать START_STICKY для перезапуска
- Обрабатывать null Intent в onStartCommand
Сравнение методов фильтрации GPS
| Метод | Точность на прямой | Точность на поворотах | Вычислительная сложность |
|---|---|---|---|
| Скользящее среднее | ±5 м | ±10 м | Низкая |
| Kalman-фильтр | ±2 м | ±4 м | Средняя |
| Системный FusedLocation | ±3 м | ±5 м | Низкая (встроен) |
Сравнение фоновой работы iOS vs Android
| Платформа | Механизм | Требования | Надёжность |
|---|---|---|---|
| iOS | Background Location | Permission «Always», UIBackgroundModes | Высокая, но может приостановить при низком заряде |
| Android | Foreground Service | Notification, START_STICKY | Очень высокая, перезапуск при убийстве |
Интеграция с носимыми устройствами
ЧСС от Apple Watch — через HKWorkoutBuilder на iOS (Watch автоматически добавляет семплы). На Android — Wear OS через HealthServicesClient или Bluetooth GATT с профилем Heart Rate (UUID 0x180D).
Bluetooth GATT для внешних датчиков (нагрудный пояс Polar H10, Wahoo TICKR):
val hrServiceUUID = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb")
val hrCharacteristicUUID = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb")
override fun onCharacteristicChanged(gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic) {
if (characteristic.uuid == hrCharacteristicUUID) {
val flag = characteristic.properties
val format = if (flag and 0x01 != 0) {
BluetoothGattCharacteristic.FORMAT_UINT16
} else {
BluetoothGattCharacteristic.FORMAT_UINT8
}
val heartRate = characteristic.getIntValue(format, 1) ?: 0
onHeartRateReceived(heartRate)
}
}
Сохранение в HealthKit / Health Connect
По завершению тренировки записываем полный HKWorkout (iOS) или ExerciseSessionRecord (Android) со всеми вложенными метриками: дистанция, ЧСС, маршрут. На iOS — HKWorkoutRouteBuilder для GPS-трека. Пользователь должен видеть тренировку в системном приложении «Здоровье» или Health Connect. Health Connect API доступен на Android 10+.
Что входит в работу
- Архитектурная документация и диаграммы состояний сессии
- Исходный код с комментариями и unit-тестами
- Интеграция с выбранными датчиками (BLE, носимые)
- Настройка фонового режима и уведомлений
- Инструкция по деплою в App Store / Google Play
- Поддержка в течение 2 месяцев после сдачи
Сроки
Базовый трекер бега с GPS, дистанцией и темпом — 3–5 недель. Полный трекер с ЧСС, BLE-датчиками, несколькими типами активностей, экспортом GPX и интеграцией в платформенные хранилища — 2–4 месяца.
Свяжитесь с нами для оценки вашего проекта — мы подберём оптимальное решение под ваш стек и бюджет. Закажите разработку трекинга под ключ и получите стабильный модуль с гарантией качества.







