iOS-Geofencing testen: Distanz, Genauigkeit und Hysterese

iOS-Geofencing testen: Distanz, Genauigkeit und Hysterese

iOS-Geofencing testen: Distanz, Genauigkeit und Hysterese

Ein Nutzer steht am Rand des Ladengeländes, doch die App protokolliert nacheinander „Betreten, Verlassen, Betreten“. Ändern Sie jetzt nicht als Erstes die Häufigkeit der Standortabfragen. Trennen Sie Distanzberechnung, Genauigkeitsfilter und Zustandswechsel voneinander und spielen Sie feste Testdaten auf einem Cloud-Mac durch. Erst dann lässt sich feststellen, ob die Geschäftsregeln instabile Zustandswechsel verursachen oder ob Systemereignisse und tatsächlicher Standort voneinander abweichen.

Geprüft wird hier die Geofencing-Logik der App, nicht ein Ersatz für die Regionsüberwachung des Systems. Alle Tests verwenden offline vorliegende Koordinaten. Sie erfassen keine Bewegungsprofile und sind unabhängig vom Standort des entfernten Rechners.

Den Vertrag für Betreten und Verlassen zuerst festlegen

Für dieses Beispiel gilt: Bei höchstens 80 Metern Abstand zum Mittelpunkt wird der Bereich als betreten gewertet, ab 120 Metern als verlassen. Dazwischen bleibt der bisherige Zustand erhalten. Die horizontale Positionsgenauigkeit muss zwischen 0 und 30 Metern liegen; andernfalls wird der Zustand für diese Messung nicht aktualisiert. Diese Zahlen definieren die Testregeln und sind keine allgemein gültigen Produktparameter.

Unterscheiden Sie außerdem zwischen „kein verlässlicher Messwert vorhanden“ und „sicher außerhalb“. Beim ersten Start ist der Zustand unbekannt. Setzen Sie ihn nicht allein deshalb auf „außerhalb“, damit die Oberfläche einen Wert anzeigen kann. Negative Genauigkeitswerte, nicht endliche Zahlen und Koordinaten außerhalb des gültigen Bereichs müssen abgewiesen werden.

Die horizontale Positionsgenauigkeit beschreibt die Qualität der Messung, nicht die tatsächliche Abweichung des Nutzers. Ziehen Sie diesen Wert nicht einfach von der Distanz ab, um daraus zu schließen, der Nutzer befinde sich sicher innerhalb des Geofence.

Die Distanzberechnung in einem eigenen Paket festschreiben

Erstellen Sie zunächst in einem gewöhnlichen Arbeitsverzeichnis auf dem Cloud-Mac ein Testpaket:

mkdir GeoPolicy
cd GeoPolicy
swift package init --type library

Ersetzen Sie Sources/GeoPolicy/GeoPolicy.swift durch die folgende Implementierung. Die Distanz auf der Kugeloberfläche wird in Metern berechnet; die Winkel werden zuvor ins Bogenmaß umgerechnet. Die trigonometrischen Funktionen verarbeiten auch Längengraddifferenzen über die Datumsgrenze hinweg korrekt. Behandeln Sie Differenzen von Breiten- und Längengraden nicht direkt als Abstände in einer Ebene.

import Foundation

struct Point {
    let latitude: Double
    let longitude: Double

    var valid: Bool {
        latitude.isFinite && longitude.isFinite &&
        (-90...90).contains(latitude) &&
        (-180...180).contains(longitude)
    }
}

func meters(_ a: Point, _ b: Point) -> Double? {
    guard a.valid, b.valid else { return nil }
    let radians = Double.pi / 180
    let p1 = a.latitude * radians
    let p2 = b.latitude * radians
    let dp = (b.latitude - a.latitude) * radians
    let dl = (b.longitude - a.longitude) * radians
    let h = pow(sin(dp / 2), 2) +
        cos(p1) * cos(p2) * pow(sin(dl / 2), 2)
    return 6_371_000 * 2 * asin(sqrt(min(1, max(0, h))))
}

Diese Näherung verwendet ein Kugelmodell. Sie eignet sich für die hier gezeigten Regeltests, ist aber kein Algorithmus für vermessungstechnische Genauigkeit. Nutzt der Produktionscode eine System-API zur Distanzberechnung, müssen die Tests dieselbe API aufrufen. Prüfen Sie nicht nur diese Hilfsimplementierung, während die App mit einer anderen Formel arbeitet.

Grenzflattern mit drei Zuständen und zwei Schwellwerten verhindern

Ergänzen Sie nun die Entscheidungsfunktion. Trifft im unbekannten Zustand ein Messwert aus dem Zwischenbereich ein, bleibt der Zustand unbekannt. Steht bereits fest, dass sich der Nutzer innerhalb oder außerhalb befindet, wird im Zwischenbereich lediglich dieser Zustand beibehalten. Ungültige Messungen dürfen weder einen Zustandswechsel noch geschäftliche Seiteneffekte wie die Vergabe einer Besuchsprämie auslösen.

enum FenceState: Equatable {
    case unknown, outside, inside
}

func nextState(
    previous: FenceState,
    distance: Double,
    accuracy: Double
) -> FenceState {
    guard distance.isFinite, distance >= 0,
          accuracy.isFinite, (0...30).contains(accuracy)
    else { return previous }

    if distance <= 80 { return .inside }
    if distance >= 120 { return .outside }
    return previous
}

Bei der Integration in die App prüfen Sie zuerst den Zeitstempel des Messwerts: Veraltete Positionen dürfen neuere nicht überschreiben. Diese Prüfung gehört in die Adapterschicht. Injizieren Sie für Tests die aktuelle Zeit, statt die reine Entscheidungslogik direkt auf die Systemuhr zugreifen zu lassen. Ein Genauigkeitswert von null ist laut API-Vertrag kein negativer Ungültigkeitsmarker. Ob er akzeptiert wird, muss anhand gesonderter, klarer fachlicher Kriterien entschieden werden.

Grenzfälle testen, ohne auf Standortmessungen zu warten

Ersetzen Sie Tests/GeoPolicyTests/GeoPolicyTests.swift durch die folgenden Tests. Für die Zustandsgrenzen werden Distanzwerte direkt übergeben. So verdecken Umrechnungsfehler bei Koordinaten nicht, was bei exakter Gleichheit mit einem Schwellwert gelten soll. Die Distanzfunktion wird separat geprüft.

import XCTest
@testable import GeoPolicy

final class GeoPolicyTests: XCTestCase {
    func testDistanceAcrossDateLine() throws {
        let a = Point(latitude: 0, longitude: 179.999)
        let b = Point(latitude: 0, longitude: -179.999)
        let distance = try XCTUnwrap(meters(a, b))
        XCTAssertEqual(distance, 222.39, accuracy: 0.5)
        XCTAssertEqual(
            distance,
            try XCTUnwrap(meters(b, a)),
            accuracy: 0.001
        )
        XCTAssertEqual(try XCTUnwrap(meters(a, a)), 0)
        XCTAssertNil(meters(
            Point(latitude: 91, longitude: 0), b
        ))
    }

    func testHysteresisAndRejectedFixes() {
        var state = FenceState.outside
        let inputs = [79.0, 85, 110, 119.9, 120, 100, 80]
        let expected: [FenceState] = [
            .inside, .inside, .inside, .inside,
            .outside, .outside, .inside
        ]
        for (distance, result) in zip(inputs, expected) {
            state = nextState(
                previous: state, distance: distance, accuracy: 10
            )
            XCTAssertEqual(state, result)
        }
        for accuracy in [-1.0, 30.01, .nan, .infinity] {
            XCTAssertEqual(nextState(
                previous: .outside, distance: 20, accuracy: accuracy
            ), .outside)
        }
        XCTAssertEqual(nextState(
            previous: .unknown, distance: 100, accuracy: 10
        ), .unknown)
        XCTAssertEqual(nextState(
            previous: .outside, distance: 80, accuracy: 30
        ), .inside)
    }
}

Führen Sie swift test aus. Die 222.39 Meter ergeben sich aus den Beispielkoordinaten und der Kugelformel. Es handelt sich nicht um Messdaten aus dem Netzwerk eines Knotens oder von einem Gerät. Ergänzen Sie anschließend Fälle mit negativen und unendlichen Distanzen, Koordinaten in den Polarregionen sowie Antipoden. Geben Sie für numerische Abweichungen explizite Toleranzen an, statt Gleitkommazahlen als Zeichenketten zu vergleichen.

Die Regeltests in die App-Pipeline einbinden

Machen Sie zunächst die Tests der reinen Logik zu einer verpflichtenden Prüfung vor dem Merge. Decken Sie anschließend die Standort-Adapterschicht im iOS-Test-Target ab: Werden alte Messwerte verworfen? Wie wird der Zustand nach dem Entzug der Standortberechtigung dargestellt? Wird bei ungültigen Messwerten lediglich der Ablehnungsgrund protokolliert? Benachrichtigungen an die Geschäftslogik dürfen nur bei gültigen Zustandsänderungen ausgelöst werden. Derselbe interne Zustand darf bei wiederholter Meldung nicht zu doppelten Buchungen führen.

Es genügt, Messwertnummer, Distanz, Genauigkeit, alten und neuen Zustand sowie den Ablehnungsgrund festzuhalten. Für reguläre Fehlerberichte müssen keine vollständigen realen Koordinaten gespeichert werden. Committen Sie die festen Testdaten gemeinsam mit der Version der Entscheidungsregeln. Werden Radien geändert, prüfen Sie gleichzeitig die erwarteten Ergebnisse.

Führen Sie abschließend eine separate Abnahme auf einem physischen Gerät durch. Geofence-Callbacks des Systems, das Aufwecken im Hintergrund und tatsächliche Ortungsfehler werden von diesen Offline-Tests nicht abgedeckt. Dokumentieren Sie „Regelprüfung bestanden“ und „Geräteverhalten bestanden“ getrennt. Ein einzelner Durchlauf im Simulator ersetzt keine dieser beiden Prüfungen.

Häufig gestellte Fragen

Warum braucht es zwei Entfernungsschwellen?

Ortungsfehler lassen Messwerte um eine einzelne Grenze schwanken. Ein kleinerer Eintrittsradius und ein größerer Austrittsradius halten den Zustand dazwischen stabil.

Ersetzen diese Tests Prüfungen auf einem Gerät?

Nein. Sie prüfen die Geschäftslogik. Systemereignisse, Hintergrundaktivierung, Berechtigungswechsel und tatsächliche Ortungsqualität müssen auf einem Gerät geprüft werden.

Ein exklusiver Cloud-Mac für Ihre Build-Pipeline

Ein kompletter physischer Mac nur für Sie – keine virtuelle Maschine. Wählen Sie Modell und Laufzeit passend zu Ihrem Build-Volumen und behalten Sie Archive, Signaturen und Caches auf Ihrem eigenen System.

Cloud-Mac konfigurieren

Ihr nächstes Archiv – erstellt auf einem exklusiven Cloud-Mac

Wählen Sie den Mac passend zu Ihrem Build-Volumen und den Standort passend zu Ihrem Team. Prüfen Sie zuerst die Optionen und übertragen Sie dann Ihre Toolchain.