附近门店突然显示在数万千米之外,未必是定位服务失效。经纬度顺序写反、跨反经线直接相减、先把距离四舍五入再判断范围,都能制造类似故障。在 GNUMac 云端 Mac 上,可以先用固定坐标把计算层跑通,再把定位输入与设备回调接回来。这样失败时有明确归属,不必反复拖动地图猜原因。
先写清坐标与距离契约
本文验证“两个坐标之间的地表距离”,不是驾车路线长度,也不代表用户实际行走距离。输入采用纬度在前、经度在后的十进制度数;内部距离统一用米,界面显示时才转换单位。
纬度必须处于负九十度至正九十度,经度必须处于负一百八十度至正一百八十度,同时拒绝非有限数。不要把非法输入悄悄裁到边界,否则上游数据损坏会变成一条看似正常的记录。
坐标合法不等于定位可信。零度纬度、零度经度是合法位置,不能无条件当作“尚未定位”;是否接受它,应由数据来源契约决定。
地图上的屏幕距离也不能作为预期值。投影、缩放与设备像素都会改变画面,却不应改变业务计算。
在云端 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)
边界样本要包含恰好相等,而不只是明显在内、明显在外。不要用显示为“零点五千米”的字符串反推结果;不同原始距离可能被舍入成同一个文本。
如果产品要减少边缘反复切换,可以定义进入与退出使用不同阈值的滞回策略。那属于状态机,需要补测连续输入序列,不能在比较器里随意加一个“差不多”的常数。
定位精度不是计算容差
真实位置样本还带有时间与水平精度。接入层应明确拒绝过期样本,并处理负的水平精度值;有效但精度较差的坐标,也不应直接生成“确认进入”事件。
一种可审核的保守规则是:设距离为 d、水平精度为 a、半径为 r。只有 d+a≤r 才判为确定在内;只有 d−a>r 才判为确定在外,其余标记不确定。它是业务上的保守判断,不是把精度值解释成绝对误差保证。
测试时分别注入精确、模糊、过期和无效样本,核对不确定状态是否保留,以及界面是否错误显示确定结论。不要通过放宽计算断言来掩盖定位噪声。
留存证据并接回 iOS 工程
上述脚本适合快速复核框架行为。正式接入时,把坐标校验与比较器放进可测试模块,由测试目标直接调用生产实现;只维护一份算法,避免脚本正确而应用仍使用旧逻辑。
每次运行留存提交标识、工具链版本、夹具坐标、原始米值及失败断言。系统升级后先检查失败属于输入校验、距离范围还是状态策略,不要直接刷新全部预期值。
最后保留一轮 iOS 真机验收:区域监测事件、后台运行和实际移动属于另一条链路。纯计算测试通过,只说明固定输入得到了符合契约的结果。将这条边界写进验收纪要,比增加随机坐标更有价值。
常见问题
云端 Mac 上的脚本通过,是否代表 iOS 围栏功能已经验收完成?
不代表。脚本验证坐标校验、距离计算与边界策略;区域监测的回调、后台行为和实际定位误差仍需在 iOS 真机上单独验收。
围栏边界能否直接使用显示出来的千米数判断?
不能。业务判断应使用未格式化的米值,显示层再换算和舍入;边界容差还应与定位精度策略分开定义。