Zurück zur Startseite

GPS-Tracker in iOS: Kalman-Filter und geohash-Implementierung

Artikel zur Implementierung eines Hintergrund-GPS-Trackers mit SwiftUI unter Verwendung von Kalman-Filter und geohash. Detaillierte Analyse technischer Lösungen für Datenverarbeitung, Routenvisualisierung und Bestehen der App Store Review.

Hintergrund-GPS-Tracker in iOS: vollständiger technischer Leitfaden
Advertisement 728x90

Hintergrund-GPS-Tracker für iOS erstellen: Technische Lösungen und Fallstricke

Nachdem Google Timeline 2024 eingestellt wurde, haben Entwickler ein zuverlässiges Tool für Routenanalysen verloren. Statt cloudbasierter Lösungen bauen wir nun lokale Apps, die den Kraftstoffverbrauch präzise erfassen. Wir führen Sie Schritt für Schritt durch die Implementierung eines GPS-Trackers in SwiftUI – vom Umgang mit verrauschten Daten über Hintergrundoptimierung bis hin zur Navigation durch die App Store Review.

Grundlagen des Hintergrund-Trackings mit CoreLocation

Die zentrale Herausforderung besteht darin, Koordinaten zu erhalten, wenn die App im Hintergrund läuft, ohne den Akku zu vernichten. Dafür benötigen Sie:

  • allowsBackgroundLocationUpdates in CLLocationManager aktivieren
  • Background Modes Entitlement mit Location-Typ hinzufügen
  • Begründung in Info.plist via NSLocationAlwaysAndWhenInUseUsageDescription angeben

Die erste Implementierung, die jeden Punkt protokollierte, führte zu einem enormen Datenvolumen (Tausende Punkte pro Fahrt). Die Optimierung gelang durch diese Parameter:

Google AdInline article slot
  • desiredAccuracy = kCLLocationAccuracyBest — priorisiert GPS vor Wi-Fi/Mobilfunkmasten
  • distanceFilter = 10 — ignoriert Bewegungen unter 10 Metern
  • Filterung nach horizontalAccuracy > 50 — filtert verrauschte Daten heraus

Diese Einstellungen reduzieren die Daten auf 200–500 Punkte pro Fahrt. Bei 60+ Routen verbraucht die Datenbank ~15 MB, was für CoreData bestens geeignet ist.

Kalman-Filter ohne IMU: Mathematik für Autobahn-Tracking

Auf Abschnitten mit GPS-Störungen (Krasnodar–Gelendzhik) sind klassische sensor-basierte Lösungen nicht verfügbar – CMMotionManager wird im Hintergrund blockiert. Wir mussten den Kalman-Filter auf reine Koordinaten anpassen:

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
    )
}

Der Algorithmus passt das Vertrauen in Messungen dynamisch an: Bei hoher horizontalAccuracy (schwaches GPS-Signal) sinkt der kalmanGain, und das System stützt sich stärker auf vorherige Daten. Bei Signalunterbrechungen länger als 10 Sekunden greifen wir auf Nachbearbeitung mit kubischen Splines zurück.

Google AdInline article slot

Routenvisualisierung nach Geschwindigkeit in SwiftUI

iOS 17+ bietet MapPolyline, ist aber auf eine Farbe pro Objekt beschränkt. Für dynamische Farbkodierung nach Geschwindigkeit mussten wir:

  • Die Spur in Segmente mit einheitlichen Geschwindigkeitsbereichen zerlegen
  • Jedes Segment als separate MapPolyline rendern
  • Benachbarte Segmente um einen Punkt überlappen
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
}

Das System nutzt 4 Farbbereiche:

  • Grün: < 50 km/h (Stadt)
  • Gelb: 50–90 km/h (Landstraßen)
  • Orange: 90–110 km/h (Autobahnen)
  • Rot: > 110 km/h

Eine typische Route erzeugt 20–50 Polylines. Bei 500+ Punkten kann es auf älteren Geräten zu Verzögerungen kommen, aber die Performance ist für die meisten Szenarien solide.

Google AdInline article slot

Geohash-Nebeleffekt: Geospatial-Optimierung

Für den „Nebeleffekt“ (Verbergen unbefahrener Abschnitte) entschieden wir uns für Geohash statt Quadtree oder direkter Punktspeicherung. Vorteile:

  • Schnelle Abfrage via Präfix-Matching
  • Einfache Serialisierung zu CoreData
  • Flexibilität via Präzisionsparameter

| Präzision | Zellgröße | Anwendung |

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

| 5 | ~4,9 × 4,9 km | Ländersicht |

| 6 | ~1,2 × 0,6 km | Stadtübersicht |

| 7 | ~150 × 150 m | Detailansicht |

Implementierung umfasst:

  • Dynamischen Präzisionswechsel basierend auf MKCoordinateSpan
  • Überprüfung sichtbarer Zellen via Set<String>
  • Animierte Auflösung mit Opazität 0,7s easeOut

Aktuelle Einschränkung: Rechteckige Zellgrenzen. Nächster Schritt: Gradienten-Rendering mit Kantenglättung für natürlicheren Look.

Wichtige Erkenntnisse

  • Eingabefilterung ist entscheidend: Ohne distanceFilter und horizontalAccuracy-Checks bläht sich die Datenbank 5–10-fach auf
  • Live Activity hat strenge Limits: 1 Hz-Updates sind das Maximum, das iOS erlaubt
  • Geohash schlägt Quadtree bei mobilen Szenarien durch einfachere Serialisierung
  • Kalman-Filter ohne IMU meistert bis zu 10 Sekunden Signalunterbrechungen
  • MapPolyline braucht Segmentierung für dynamische Farben – eine Route = 20–50 Objekte

Live Activity: Gegen menschliches Versagen

Problem: Nutzer vergessen, die Aufzeichnung zu stoppen, und entleeren den Akku. Lösung: Sperrbildschirm-Indikator via ActivityKit:

// Activity starten
let activity = try Activity.request(
    attributes: TripActivityAttributes(carName: "Polo Sedan"),
    content: .init(state: initialState)
)

// Jede Sekunde aktualisieren
await activity.update(.init(state: updatedState))

Wichtiger Punkt: Die Update-Frequenz ist auf ~1 Hz begrenzt. Häufigere Übertragungen werden vom System ignoriert. Das reicht für ein Geschwindigkeitsmesser, aber hochfrequente Metriken brauchen Datenaggregation.

Datenspeicherung in CoreData

Das Datenbankschema ist für schnellen Zugriff auf Routen und Punkte optimiert:

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

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

ExploredCellEntity
    └── geohash (unique)

Implementierungsdetails:

  • NSMergeByPropertyObjectTrumpMergePolicy für Geohash-Zellen
  • Automatische Löschung alter Einträge via Timestamp
  • Indizierung nach tripID und Timestamp für schnellere Abfragen

Bei 500+ Punkten pro Route ist Minimierung von Fetch-Requests essenziell. Alle LocationPointEntity-Operationen laufen in einem einzigen Managed Object Context mit Batch-Updates.

— Editorial Team

Advertisement 728x90

Weiterlesen