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:
allowsBackgroundLocationUpdatesin CLLocationManager aktivieren- Background Modes Entitlement mit Location-Typ hinzufügen
- Begründung in Info.plist via
NSLocationAlwaysAndWhenInUseUsageDescriptionangeben
Die erste Implementierung, die jeden Punkt protokollierte, führte zu einem enormen Datenvolumen (Tausende Punkte pro Fahrt). Die Optimierung gelang durch diese Parameter:
desiredAccuracy = kCLLocationAccuracyBest— priorisiert GPS vor Wi-Fi/MobilfunkmastendistanceFilter = 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.
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.
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
Noch keine Kommentare.