Un même utilisateur reste à la limite d’un magasin, mais l’application enregistre successivement « entrée, sortie, entrée ». Avant de modifier la fréquence de localisation, séparez le calcul de distance, le filtrage par précision et les transitions d’état. Rejouez ensuite des échantillons fixes sur un Mac dans le cloud pour déterminer si cette oscillation vient des règles métier ou d’un décalage entre les événements système et la position réelle.
La validation porte ici sur la politique de géorepérage propre à l’application, sans remplacer la surveillance de régions assurée par le système. Tout repose sur des coordonnées hors ligne : aucun trajet utilisateur n’est collecté et l’emplacement physique du Mac distant n’intervient pas.
Définir d’abord les règles d’entrée et de sortie
Dans cet exemple, l’entrée n’est reconnue qu’à une distance inférieure ou égale à 80 mètres du centre ; la sortie l’est à partir de 120 mètres. Entre les deux, l’état précédent est conservé. La précision horizontale de la position doit être comprise entre 0 et 30 mètres ; sinon, l’échantillon ne met pas l’état à jour. Ces valeurs définissent une politique de test, pas des paramètres universels à appliquer en production.
Il faut aussi distinguer « aucun échantillon fiable » de « présence confirmée à l’extérieur ». Au premier lancement, l’état doit être inconnu : ne le définissez pas par défaut sur « à l’extérieur » simplement pour afficher une valeur dans l’interface. Rejetez les précisions négatives, les valeurs non finies et les coordonnées hors limites.
La précision horizontale renseigne sur la qualité de la mesure, pas sur le décalage réel de l’utilisateur. Ne la soustrayez pas directement de la distance pour affirmer ensuite que l’utilisateur se trouve nécessairement dans le périmètre.
Isoler le calcul de distance dans un package autonome
Commencez par créer un package de test dans un répertoire de travail ordinaire sur le Mac dans le cloud :
mkdir GeoPolicy
cd GeoPolicy
swift package init --type library
Remplacez Sources/GeoPolicy/GeoPolicy.swift par l’implémentation suivante. La distance sphérique est exprimée en mètres et les angles sont d’abord convertis en radians. Les fonctions trigonométriques gèrent correctement les écarts de longitude qui traversent la ligne de changement de date ; ne traitez pas directement les différences de latitude et de longitude comme une distance dans un plan.
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))))
}
Cette approximation sphérique convient aux tests de politique présentés ici ; ce n’est pas un algorithme de précision topographique. Si le code de production utilise l’API système de calcul de distance, les tests doivent appeler cette même API. Ne validez pas uniquement cette fonction auxiliaire si l’application exécute une autre formule.
Stabiliser la frontière avec trois états et deux seuils
Ajoutez ensuite la fonction qui applique la politique. Un état inconnu reste inconnu lorsqu’un échantillon tombe dans la bande intermédiaire. Si l’état est déjà intérieur ou extérieur, cette bande ne fait que conserver la valeur précédente. Une mesure invalide ne doit déclencher ni transition d’état ni effet métier, comme l’attribution d’une récompense pour une visite en magasin.
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
}
Lors de l’intégration réelle, vérifiez d’abord l’horodatage de l’échantillon : une position périmée ne doit pas écraser une position plus récente. Placez ce contrôle dans la couche d’adaptation et injectez l’heure courante pour les tests, au lieu de lire directement l’horloge système dans la logique pure de la politique. Selon le contrat de l’API, une précision de zéro n’est pas une valeur négative signalant une mesure invalide. Son acceptation doit reposer sur une justification métier explicite et distincte.
Tester les limites sans attendre de nouvelles positions
Remplacez Tests/GeoPolicyTests/GeoPolicyTests.swift par les tests ci-dessous. Les tests des seuils d’état utilisent directement des distances pour éviter que les erreurs de conversion des coordonnées ne masquent le traitement de l’égalité. La fonction de distance est vérifiée séparément.
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)
}
}
Exécutez swift test. La valeur de 222.39 mètres provient des coordonnées de l’exemple et de la formule sphérique, et non de mesures effectuées sur le réseau d’un nœud ou sur un appareil. Complétez ensuite les cas avec des distances négatives ou infinies, des coordonnées polaires et des points antipodaux. Exprimez les erreurs numériques au moyen de tolérances explicites, sans comparer les représentations textuelles des nombres à virgule flottante.
Raccorder les tests de règles au pipeline de l’application
Intégrez d’abord les tests de logique pure aux contrôles avant fusion, puis couvrez la couche d’adaptation de la localisation dans la cible de tests iOS : rejet des anciens échantillons, présentation de l’état après révocation de l’autorisation et simple consignation du motif pour les échantillons invalides. Les notifications métier ne doivent être émises que lors d’une transition d’état valide ; la répétition d’un même état interne ne doit pas entraîner de double comptabilisation.
Il suffit de conserver le numéro de séquence de l’échantillon, la distance, la précision, l’ancien état, le nouvel état et le motif du rejet. Les rapports d’échec courants n’ont pas besoin de contenir les coordonnées réelles complètes. Versionnez les échantillons fixes avec la politique et réexaminez les résultats attendus à chaque modification des rayons.
Enfin, effectuez une validation distincte sur un appareil physique : les callbacks système de géorepérage, les réveils en arrière-plan et les erreurs de localisation réelles ne sont pas couverts par ces tests hors ligne. Consignez séparément « règles validées » et « comportement sur appareil validé » ; une seule exécution dans le simulateur ne remplace ni l’une ni l’autre.
Questions fréquentes
Pourquoi prévoir deux seuils de distance ?
Une position bruitée peut franchir sans cesse un seuil unique. Un rayon d’entrée plus petit et un rayon de sortie plus grand conservent l’état entre les deux.
Les tests unitaires remplacent-ils les essais sur appareil ?
Non. Ils valident la logique métier. Les événements système, le réveil en arrière-plan, les autorisations et la précision réelle exigent des essais sur appareil.
Un Mac cloud dédié pour vos builds
Une machine physique entièrement dédiée, pas une machine virtuelle. Choisissez le modèle et la durée selon votre volume de builds, et gardez archives, signatures et caches sur votre propre nœud.
Choisir une configuration Mac cloud