Retour à l'accueil

Traceur GPS dans iOS : implémentation du filtre de Kalman et de geohash

Article sur l'implémentation d'un traceur GPS en arrière-plan sur SwiftUI utilisant le filtre de Kalman et geohash. Analyse détaillée des solutions techniques pour le traitement des données, la visualisation d'itinéraire et le passage de l'examen App Store.

Traceur GPS en arrière-plan dans iOS : guide technique complet
Advertisement 728x90

Comment créer un traqueur GPS en arrière-plan sur iOS : Solutions techniques et pièges

Après l'arrêt de Google Timeline en 2024, les développeurs ont perdu un outil fiable pour l'analyse des itinéraires. Au lieu de solutions basées sur le cloud, nous développons désormais des applications locales qui suivent précisément la consommation de carburant. Nous allons détailler l'implémentation d'un traqueur GPS en SwiftUI, en couvrant la gestion des données bruitées, l'optimisation en arrière-plan et la traversée du processus de révision de l'App Store.

Bases du suivi en arrière-plan avec CoreLocation

Le défi principal consiste à obtenir des coordonnées lorsque l'app est en arrière-plan sans vider la batterie. Voici ce qu'il faut :

  • Activer allowsBackgroundLocationUpdates dans CLLocationManager
  • Ajouter l'entitlement Background Modes avec le type location
  • Fournir une justification dans Info.plist via NSLocationAlwaysAndWhenInUseUsageDescription

L'implémentation initiale, qui enregistrait chaque point, générait un volume de données excessif (milliers de points par trajet). L'optimisation est venue de la combinaison de ces paramètres :

Google AdInline article slot
  • desiredAccuracy = kCLLocationAccuracyBest — privilégie le GPS aux tours Wi-Fi/cellulaires
  • distanceFilter = 10 — ignore les déplacements inférieurs à 10 mètres
  • Filtrage par horizontalAccuracy > 50 — élimine les données bruitées

Ces réglages réduisent les données à 200–500 points par trajet. Avec plus de 60 trajets, la base de données occupe ~15 Mo, ce qui est acceptable pour CoreData.

Filtre de Kalman sans IMU : Mathématiques pour le suivi sur autoroute

Sur les tronçons avec brouillage GPS (Krasnodar–Gelendzhik), les solutions classiques basées sur capteurs ne sont pas disponibles — CMMotionManager est bloqué en arrière-plan. Nous avons adapté le filtre de Kalman pour fonctionner uniquement avec les coordonnées :

func processLocation(_ location: CLLocation) -> CLLocation {
    guard let previous = lastFilteredLocation else {
        lastFilteredLocation = location
        return location
    }

    let dt = location.timestamp.timeIntervalSince(previous.timestamp)
    let processNoise = dt * speedVariance
    predictedVariance += processNoise

    let measurementVariance = location.horizontalAccuracy * location.horizontalAccuracy
    let kalmanGain = predictedVariance / (predictedVariance + measurementVariance)

    let lat = previous.coordinate.latitude
        + kalmanGain * (location.coordinate.latitude - previous.coordinate.latitude)
    let lon = previous.coordinate.longitude
        + kalmanGain * (location.coordinate.longitude - previous.coordinate.longitude)

    predictedVariance = (1 - kalmanGain) * predictedVariance

    return CLLocation(
        coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon),
        altitude: location.altitude,
        horizontalAccuracy: sqrt(predictedVariance),
        verticalAccuracy: location.verticalAccuracy,
        timestamp: location.timestamp
    )
}

L'algorithme ajuste dynamiquement la confiance dans les mesures : avec une horizontalAccuracy élevée (mauvais signal GPS), le kalmanGain diminue, et le système s'appuie davantage sur les données précédentes. Pour les pertes de signal supérieures à 10 secondes, nous utilisons un post-traitement avec des splines cubiques.

Google AdInline article slot

Visualisation des itinéraires par vitesse en SwiftUI

iOS 17+ propose MapPolyline, mais limitée à une couleur par objet. Pour une coloration dynamique basée sur la vitesse, nous avons dû :

  • Diviser la trace en segments de plages de vitesse uniformes
  • Rendre chaque segment comme une MapPolyline séparée
  • Chevaucher les segments adjacents d'un point
struct SpeedSegment {
    let coordinates: [CLLocationCoordinate2D]
    let color: Color
}

func buildSegments(from points: [LocationPoint]) -> [SpeedSegment] {
    var segments: [SpeedSegment] = []
    var currentCoords: [CLLocationCoordinate2D] = []
    var currentColor: Color = .green

    for point in points {
        let color = speedColor(for: point.speed)
        if color != currentColor && !currentCoords.isEmpty {
            segments.append(SpeedSegment(
                coordinates: currentCoords,
                color: currentColor
            ))
            currentCoords = [currentCoords.last!]
        }
        currentCoords.append(point.coordinate)
        currentColor = color
    }

    if !currentCoords.isEmpty {
        segments.append(SpeedSegment(
            coordinates: currentCoords,
            color: currentColor
        ))
    }
    return segments
}

Le système utilise 4 plages de couleurs :

  • Vert : < 50 km/h (ville)
  • Jaune : 50–90 km/h (autoroutes)
  • Orange : 90–110 km/h (voies rapides)
  • Rouge : > 110 km/h

Un trajet typique génère 20–50 polylignes. Avec 500+ points, il peut y avoir du lag sur les anciens appareils, mais les performances sont solides pour la plupart des cas.

Google AdInline article slot

Geohash pour brouillard de guerre : Optimisation géospatiale

Pour l'effet de « brouillard de guerre » (cacher les sections non parcourues), nous avons choisi geohash plutôt que quadtree ou stockage direct de points. Avantages :

  • Recherche rapide par correspondance de préfixe
  • Sérialisation simple vers CoreData
  • Flexibilité via paramètre de précision

| Précision | Taille de cellule | Application |

|-----------|-------------------|----------------------|

| 5 | ~4,9 × 4,9 km | Vue d'ensemble pays |

| 6 | ~1,2 × 0,6 km | Vue d'ensemble ville |

| 7 | ~150 × 150 m | Vue détaillée |

L'implémentation inclut :

  • Changement dynamique de précision basé sur MKCoordinateSpan
  • Vérification des cellules visibles via Set<String>
  • Animation de dissipation avec opacité 0,7 s easeOut

Limitation actuelle : frontières de cellules rectangulaires. Prochainement : rendu de gradient avec flou des bords pour un aspect plus naturel.

Points clés

  • Le filtrage d'entrée est crucial : sans distanceFilter et vérifications horizontalAccuracy, la base de données gonfle de 5–10x
  • Live Activity a des limites strictes : 1 Hz est la fréquence max autorisée par iOS
  • Geohash surpasse quadtree pour les scénarios mobiles grâce à une sérialisation plus simple
  • Filtre de Kalman sans IMU gère jusqu'à 10 secondes de pertes de signal
  • MapPolyline nécessite une segmentation pour la coloration dynamique — un trajet = 20–50 objets

Live Activity : Combattre l'erreur humaine

Problème : les utilisateurs oublient d'arrêter l'enregistrement, vidant la batterie. Solution : un indicateur sur l'écran de verrouillage via ActivityKit :

// Lancer l'activité
let activity = try Activity.request(
    attributes: TripActivityAttributes(carName: "Polo Sedan"),
    content: .init(state: initialState)
)

// Mise à jour toutes les secondes
await activity.update(.init(state: updatedState))

Point clé : la fréquence de mise à jour est limitée à ~1 Hz. Envoyer plus souvent est ignoré par le système. C'est suffisant pour un compteur de vitesse, mais les métriques haute fréquence nécessitent une agrégation de données.

Stockage des données dans CoreData

Le schéma de base de données est optimisé pour un accès rapide aux trajets et points :

TripEntity (1) ──── (*) LocationPointEntity
    ├── startDate
    ├── distance
    └── fuelCost

LocationPointEntity
    ├── latitude
    ├── longitude
    └── speed

ExploredCellEntity
    └── geohash (unique)

Détails d'implémentation :

  • NSMergeByPropertyObjectTrumpMergePolicy pour les cellules geohash
  • Suppression automatique des anciens enregistrements via timestamp
  • Indexation par tripID et timestamp pour des requêtes plus rapides

Avec 500+ points par trajet, minimiser les fetch requests est crucial. Toutes les opérations LocationPointEntity s'exécutent dans un seul managed object context avec des mises à jour par lots.

— Editorial Team

Advertisement 728x90

Lire ensuite