云端 Mac 上的 iOS 地理围栏判定:距离与迟滞测试

云端 Mac 上的 iOS 地理围栏判定:距离与迟滞测试

同一个用户站在门店边缘,应用却连续记录“进入、离开、进入”。先别调整定位频率。把距离计算、精度过滤和状态切换拆开,在云端 Mac 上用固定样本重放,才能判断是业务规则抖动,还是系统事件与实际位置不一致。

下面验收的是应用自己的围栏策略,不是替代系统区域监测。全程使用离线坐标,不采集用户轨迹,也不依赖远程机器所在地。

先写清进入与离开的契约

示例规定:距离中心不超过 80 米才进入;达到 120 米才离开;中间区域保持原状态。定位水平精度必须在 0 至 30 米之间,否则本次不更新状态。这些数字是测试策略,不是通用产品参数。

还要区分“没有可信样本”和“确定在外面”。首次启动用未知状态,不能为了让界面有值就默认为已离开。负数精度、非有限数及越界坐标都应拒绝。

水平精度是测量质量信息,不是用户实际偏移距离。不要把它直接从距离中减掉,再宣称用户一定在围栏里。

在独立包中固定距离计算

先在云端 Mac 的普通工作目录创建测试包:

mkdir GeoPolicy
cd GeoPolicy
swift package init --type library

将 Sources/GeoPolicy/GeoPolicy.swift 替换为以下实现。球面距离使用米,角度先转弧度;三角函数可以正确处理跨日期变更线的经度差,不要直接把经纬度差当平面距离。

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))))
}

这是球面近似,适合本文的策略测试,不是测绘级算法。如果生产代码使用系统距离接口,测试应调用同一接口;不要只验证这份辅助实现,却让应用运行另一套公式。

用三态与双阈值消除边界抖动

继续添加策略函数。未知状态遇到中间带样本仍然未知;已经在内或在外时,中间带只保留旧值。无效测量不触发状态转移,也不应触发到店奖励之类的业务副作用。

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
}

真实接入时先校验样本时间:过期位置不能覆盖更新的位置。时间校验放在适配层,并注入当前时间测试,不要在纯策略里直接读取系统时钟。精度为零在接口约定中不属于负值无效标记;是否接受它,应另有明确业务依据。

让测试覆盖边界而不依赖定位等待

将 Tests/GeoPolicyTests/GeoPolicyTests.swift 替换为下面的测试。状态边界直接使用距离值,避免坐标换算误差掩盖等号语义;距离函数则单独验证。

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)
    }
}

运行 swift test。这里的 222.39 米来自示例坐标与球面公式,不是节点网络或设备实测数据。后续补入负距离、无穷距离、极区坐标和对跖点样本;数值误差用明确容差表达,不要比较浮点字符串。

把规则测试接回应用流水线

先让纯逻辑测试成为合并前检查,再在 iOS 测试目标里覆盖定位适配层:旧样本被丢弃、授权撤销后状态如何呈现、无效样本是否只记录原因。业务通知只在有效状态发生变化时发出,重复的内部状态不能重复记账。

保留样本序号、距离、精度、旧状态、新状态和拒绝原因即可;日常失败报告不需要保存完整真实坐标。固定样本与策略版本一起提交,修改半径时同步审查预期结果。

最后单独做真机验收:系统围栏回调、后台唤醒和实际定位误差不是这组离线测试的覆盖范围。验收结果应分开记录“规则通过”和“设备行为通过”,不要用一次模拟器运行替代两者。

常见问题

围栏边缘为什么不能只用一个距离阈值?

定位误差会使连续样本在同一阈值两侧跳动。进入使用较小阈值、退出使用较大阈值,中间区域保留原状态,可以减少重复切换。

这些测试通过后,还需要真机验收吗?

需要。离线测试只覆盖业务距离与状态规则,系统围栏事件、后台唤醒、授权变化和实际定位质量仍须分别在真机上验收。

给构建流程一台独享的云端 Mac

整台物理机独享,非虚拟机。按构建量选择机型与租期,让归档、签名和缓存留在自己的节点上。

选择云端 Mac 配置

下一次归档,在独享的云端 Mac 上出炉

按构建量选机器,按团队位置选节点。先核对方案,再开始交接工具链。