# Jak vytvořit GPS tracker na pozadí v iOS: technická řešení a úskalí
Po deaktivaci Google Timeline v roce 2024 přišli vývojáři o spolehlivý nástroj pro analýzu tras. Namísto cloudových řešení je nutné vytvářet lokální aplikace s přesným sledováním spotřeby paliva. Probereme implementaci GPS trackeru ve SwiftUI, včetně zpracování šumivých dat, optimalizace práce na pozadí a úspěšného projití recenze App Store.
Základy pozadového trackingu v CoreLocation
Klíčovým problémem je získávání souřadnic při minimalizovaném aplikaci bez vybíjení baterie. K tomu je potřeba:
- Aktivovat
allowsBackgroundLocationUpdatesv CLLocationManager - Přidat entitlement Background Modes s typem location
- Uvést motivaci v Info.plist přes
NSLocationAlwaysAndWhenInUseUsageDescription
Počáteční implementace s ukládáním každé body vedla k nadměrnému objemu dat (tisíce bodů na cestu). Optimalizace byla dosažena kombinací parametrů:
desiredAccuracy = kCLLocationAccuracyBest— volba GPS místo Wi-Fi/buněčných věžídistanceFilter = 10— ignorování posunů menších než 10 metrů- Filtrování podle
horizontalAccuracy > 50— odstraňování šumivých dat
Tyto nastavení snížily objem dat na 200–500 bodů na cestu. Při 60+ trasách databáze zabírá ~15 MB, což je přijatelné pro CoreData.
Kalmanův filtr bez IMU: matematika pro silniční trasy
Na úsecích s rušením GPS (Krasnodar–Gelendžik) nejsou klasická řešení s senzory dostupná — CMMotionManager je blokován v pozadovém režimu. Bylo nutné adaptovat Kalmanův filtr pro práci pouze se souřadnicemi:
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
)
}
Algoritmus dynamicky upravuje důvěru k měřením: při vysoké horizontalAccuracy (špatný GPS signál) se kalmanGain snižuje a systém se více spoléhá na předchozí data. Pro výpadky signálu delší než 10 sekund se používá post-processing kubickými splajny.
Vizualizace trasy podle rychlosti ve SwiftUI
iOS 17+ poskytuje MapPolyline, ale jeho omezení je jedna barva na objekt. Pro dynamické zbarvení trasy podle rychlosti bylo potřeba:
- Rozdělit stopu na segmenty se stejným rozsahem rychlostí
- Každý segment vykreslit samostatným MapPolyline
- Zajistit překrytí sousedních segmentů jedním bodem
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
}
Systém používá 4 barevné rozsahy:
- Zelená: < 50 km/h (město)
- Žlutá: 50–90 km/h (dálnice)
- Oranžová: 90–110 km/h (silnice)
- Červená: > 110 km/h
Na typické trase vznikne 20–50 polylinií. Při 500+ bodech jsou na starších zařízeních patrné zpoždění, ale pro většinu scénářů je výkon dostatečný.
Geohash Fog of War: geoprostřední optimalizace
Pro efekt „mlhy války“ (skrytí neprojetých úseků) byl zvolen geohash místo quadtree nebo přímého ukládání bodů. Výhody:
- Rychlé hledání přes prefixové porovnání
- Jednoduchá serializace do CoreData
- Flexibilita přes parametr precision
| Precision | Velikost buňky | Použití |
|-----------|----------------|---------|
| 5 | ~4,9 × 4,9 km | Přehled země |
| 6 | ~1,2 × 0,6 km | Přehled města |
| 7 | ~150 × 150 m | Detailní pohled |
Implementace zahrnuje:
- Dynamické přepínání precision podle MKCoordinateSpan
- Kontrolu viditelných buněk přes Set<String>
- Animaci rozptylu s opacity 0.7s easeOut
Aktuální omezení — obdélníkové hranice buněk. V plánech přechod na gradientový rendering s rozmazáním okrajů pro přirozený vzhled.
Co je důležité
- Filtrace na vstupu je kritická: bez distanceFilter a filtru horizontalAccuracy se databáze nafoukne 5–10krát
- Live Activity má přísné limity: aktualizace jednou za sekundu — maximum, co iOS dovolí
- Geohash je efektivnější než quadtree pro mobilní scénáře díky jednoduché serializaci
- Kalmanův filtr bez IMU funguje až do 10sekundových výpadků signálu
- MapPolyline vyžaduje segmentaci pro dynamické zbarvení — jedna trasa = 20–50 objektů
Live Activity: boj s lidským faktorem
Problém: uživatelé zapomínají zastavit záznam, což vede k vybití baterie. Řešení — indikátor na zamykací obrazovce přes ActivityKit:
// Spuštění aktivity
let activity = try Activity.request(
attributes: TripActivityAttributes(carName: "Polo Sedan"),
content: .init(state: initialState)
)
// Aktualizace každou sekundu
await activity.update(.init(state: updatedState))
Důležité: frekvence aktualizací je omezena na ~1 Hz. Pokusy o častější odesílání vedou k ignorování požadavků systémem. Pro spidometr to stačí, ale pro vysokočetné metriky bude nutná agregace dat.
Ukládání dat v CoreData
Schéma databáze je optimalizováno pro rychlý přístup k trasám a bodům:
TripEntity (1) ──── (*) LocationPointEntity
├── startDate
├── distance
└── fuelCost
LocationPointEntity
├── latitude
├── longitude
└── speed
ExploredCellEntity
└── geohash (unique)
Specifika implementace:
- Použití NSMergeByPropertyObjectTrumpMergePolicy pro geohash buňky
- Automatické mazání starých záznamů přes timestamp
- Indexování podle tripID a timestamp pro zrychlení dotazů
Při práci se 500+ body na trasu je kritické minimalizovat počet fetch požadavků. Všechny operace s LocationPointEntity probíhají v jednom managed object context s batch aktualizacemi.
— Editorial Team
Zatím žádné komentáře.