Проверяем расстояния iOS и границы геозон на облачном Mac
Если ближайший магазин внезапно оказался в десятках тысяч километров, причина не обязательно в сбое геолокации. Перепутанные широта и долгота, прямое вычитание долгот при пересечении антимеридиана или округление расстояния до проверки порога — всё это может привести к похожим ошибкам. На облачном Mac GNUMac можно сначала проверить расчёты на фиксированных координатах, а затем подключить данные геолокации и обратные вызовы устройства. Тогда при сбое будет понятно, на каком уровне искать причину, и не придётся снова и снова перетаскивать карту наугад.
Сначала определите контракт координат и расстояний
Здесь мы проверяем расстояние между двумя координатами по поверхности Земли, а не длину автомобильного маршрута и не фактически пройденный пользователем путь. На вход подаются координаты в десятичных градусах: сначала широта, затем долгота. Все внутренние расчёты ведутся в метрах; преобразование единиц выполняется только для отображения в интерфейсе.
Широта должна находиться в диапазоне от минус девяноста до плюс девяноста градусов, долгота — от минус ста восьмидесяти до плюс ста восьмидесяти градусов. Неконечные значения также нужно отклонять. Не стоит молча подгонять недопустимые значения под границы диапазона: так повреждённые входные данные превратятся в запись, которая выглядит корректной.
Допустимые координаты ещё не означают, что данным геолокации можно доверять. Нулевая широта и нулевая долгота задают допустимую точку, поэтому нельзя безусловно считать её признаком того, что местоположение ещё не определено. Принимать ли такую точку, должен определять контракт источника данных.
Расстояние между точками на экране карты тоже не годится в качестве ожидаемого результата. Проекция, масштаб и пиксельная плотность устройства меняют изображение, но не должны влиять на расчёты бизнес-логики.
Запустите фиксированные тестовые данные на облачном Mac
Подготовьте среду macOS с выбранным набором инструментов разработки и выполните xcrun swift --version, чтобы убедиться в доступности компилятора. Сохраните следующий код в файл distance-check.swift. Скрипт не считывает текущее местоположение, не требует разрешения на геолокацию и не обращается к сети.
import Foundation
import CoreLocation
enum CoordinateError: Error {
case invalid
}
func point(_ latitude: Double, _ longitude: Double) throws -> CLLocation {
let coordinate = CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude
)
guard latitude.isFinite,
longitude.isFinite,
CLLocationCoordinate2DIsValid(coordinate) else {
throw CoordinateError.invalid
}
return CLLocation(latitude: latitude, longitude: longitude)
}
func rejects(_ latitude: Double, _ longitude: Double) -> Bool {
do {
_ = try point(latitude, longitude)
return false
} catch {
return true
}
}
let origin = try point(0, 0)
precondition(origin.distance(from: origin) == 0)
let east = try point(0, 179.9)
let west = try point(0, -179.9)
let crossing = east.distance(from: west)
precondition(crossing > 20_000 && crossing < 23_000)
precondition(abs(crossing - west.distance(from: east)) < 1)
let polarA = try point(89.9, 0)
let polarB = try point(89.9, 180)
let polarDistance = polarA.distance(from: polarB)
precondition(polarDistance > 20_000 && polarDistance < 23_000)
precondition(rejects(91, 0))
precondition(rejects(0, 181))
precondition(rejects(.nan, 0))
precondition(rejects(0, .infinity))
Запустите скрипт следующей командой. Если все проверки пройдут, вывода не будет. При сбое процесс завершится с ненулевым кодом возврата, поэтому скрипт удобно использовать как отдельный шаг конвейера.
xcrun swift distance-check.swift
Указанные диапазоны — это широкие допуски для тестовых данных, а не результаты измерения производительности и не обещание точности для бизнес-задач. Точки по разные стороны антимеридиана находятся близко друг к другу; большая разница долгот в полярной области тоже не означает расстояния в половину земного шара. Обе проверки выявляют ошибку, при которой широту и долготу напрямую вычитают как координаты на плоскости.
Вынесите проверку границы геозоны в чистую функцию
Предположим, бизнес-правило гласит: точка считается внутри, если расстояние меньше радиуса или равно ему. Сначала проверьте функцию сравнения на заданных вручную расстояниях и только затем подключайте результаты расчётов по координатам. Так не придётся отлаживать два уровня одновременно. Добавьте следующий код в конец того же файла и запустите его повторно.
func isInside(distance: Double, radius: Double) -> Bool {
guard distance.isFinite, radius.isFinite,
distance >= 0, radius >= 0 else {
return false
}
return distance <= radius
}
precondition(isInside(distance: 499.9, radius: 500))
precondition(isInside(distance: 500, radius: 500))
precondition(!isInside(distance: 500.1, radius: 500))
precondition(!isInside(distance: .nan, radius: 500))
precondition(!isInside(distance: 0, radius: -1))
let meters = 1_250.0
precondition(meters / 1_000 == 1.25)
Граничные тесты должны включать точное равенство, а не только случаи, явно находящиеся внутри или снаружи. Не определяйте результат по строке интерфейса «0,5 км»: разные исходные расстояния могут округляться до одного и того же текста.
Если продукту нужно уменьшить частые переключения у границы, можно задать гистерезис — разные пороги для входа и выхода. Но это уже логика конечного автомата, для которой нужны дополнительные тесты последовательностей входных данных. Нельзя просто добавить в функцию сравнения произвольную константу со смыслом «примерно равно».
Точность геолокации — не допуск вычислений
Реальные данные о местоположении содержат также временную метку и значение горизонтальной точности. Уровень приёма данных должен явно отклонять устаревшие измерения и обрабатывать отрицательные значения горизонтальной точности. Допустимые координаты с низкой точностью тоже не должны сразу порождать событие «вход подтверждён».
Можно использовать следующее консервативное правило, которое легко проверить: пусть d — расстояние, a — горизонтальная точность, r — радиус. Точка считается определённо внутри только при d+a≤r и определённо снаружи только при d−a>r. Во всех остальных случаях состояние помечается как неопределённое. Это консервативное бизнес-правило, а не трактовка точности как гарантированной абсолютной границы ошибки.
В тестах отдельно подавайте точные, неточные, устаревшие и недопустимые данные. Проверяйте, сохраняется ли неопределённое состояние и не показывает ли интерфейс однозначный вывод там, где его быть не должно. Не маскируйте шум геолокации ослаблением проверок вычислений.
Сохраните результаты и подключите проверки к проекту iOS
Описанный скрипт подходит для быстрой проверки поведения фреймворка. При полноценной интеграции вынесите проверку координат и функцию сравнения в тестируемый модуль, а из тестового таргета вызывайте непосредственно рабочую реализацию. Поддерживайте только одну версию алгоритма, чтобы не получить ситуацию, когда скрипт работает правильно, а приложение по-прежнему использует старую логику.
Для каждого запуска сохраняйте идентификатор коммита, версию набора инструментов, тестовые координаты, исходные значения в метрах и проверки, завершившиеся сбоем. После обновления системы сначала выясните, относится ли сбой к проверке входных данных, диапазону расстояний или правилам перехода между состояниями. Не обновляйте сразу все ожидаемые результаты.
В завершение обязательно проведите отдельную приёмочную проверку на реальном устройстве iOS: события мониторинга регионов, работа в фоне и фактическое перемещение относятся к другой цепочке. Успешные вычислительные тесты означают лишь то, что фиксированные входные данные дали результат, соответствующий контракту. Зафиксировать это ограничение в протоколе приёмки полезнее, чем добавить ещё случайных координат.
Часто задаваемые вопросы
Достаточно ли успешного скрипта на Mac для проверки геозон iOS?
Нет. Скрипт проверяет координаты, расчёт расстояния и правила сравнения с порогом. События регионов, фоновую работу и реальную точность нужно отдельно проверять на устройстве iOS.
Можно ли определять вход в геозону по отображаемому числу километров?
Нет. Используйте исходное расстояние в метрах. Преобразование единиц и округление оставьте интерфейсу, а обработку точности геопозиции задайте отдельно.
Настройте выделенный Mac mini под свой рабочий процесс
Выберите модель, узел и срок аренды под задачи проекта. Сначала проверьте требования к памяти, хранилищу и подключению, затем оформите заказ. Фактическая доступность отображается в консоли в реальном времени.