云端 Mac 上的 iOS 网络状态提示验收:NWPathMonitor 与离线判定边界

云端 Mac 上的 iOS 网络状态提示验收:NWPathMonitor 与离线判定边界

用户点刷新时,界面提示“网络不可用”,稍后重试却成功了。常见原因不是请求层失灵,而是应用把尚未更新的网络路径状态当成了请求准入条件。在云端 Mac 上开发 iOS 应用时,可以先用确定性的事件序列验收状态模型,再把模拟器和真机留给系统行为检查,不必反复断开远程连接来制造故障。

先把提示与请求结果拆开

NWPathMonitor 观察的是符合监测条件的网络路径。.satisfied 不保证服务器在线,也不保证认证有效;.requiresConnection 表示需要建立连接,不能直接解释为永久离线。

产品层至少保留“未知、路径可用、需要连接、路径不可用”四种状态。初次启动尚未收到回调时显示未知,不要默认离线。接口超时、认证失败和服务端错误分别进入请求结果模型,不要反向覆盖路径状态。

网络状态适合提供提示,不适合充当“禁止发送所有请求”的总开关。用户主动重试应交给实际请求验证。

这套做法适合需要网络横幅、自动重试提示的应用;如果功能只是发送一次请求,直接处理请求结果往往更简单,无须额外增加监听。

建立可独立运行的状态模型

在远程 macOS 命令行创建 NetworkStateCheck.swift,写入下面的完整内容。模型不依赖真实网络,代次用于区分不同监听实例;示例使用串行调用,不能直接从多个队列并发修改。

enum PathState: Equatable {
    case unknown, available, needsConnection, unavailable
}

struct NetworkHint {
    private(set) var generation = 0
    private(set) var state: PathState = .unknown
    private(set) var expensive = false
    private(set) var constrained = false

    mutating func restart() -> Int {
        generation += 1
        state = .unknown
        expensive = false
        constrained = false
        return generation
    }

    mutating func accept(
        generation incoming: Int,
        state next: PathState,
        expensive: Bool = false,
        constrained: Bool = false
    ) {
        guard incoming == generation else { return }
        state = next
        self.expensive = expensive
        self.constrained = constrained
    }
}

var hint = NetworkHint()
precondition(hint.state == .unknown)

let old = hint.restart()
hint.accept(generation: old, state: .available, expensive: true)
precondition(hint.state == .available && hint.expensive)

let current = hint.restart()
hint.accept(generation: old, state: .unavailable)
precondition(hint.state == .unknown)

hint.accept(generation: current, state: .needsConnection)
precondition(hint.state == .needsConnection)

hint.accept(generation: current, state: .available, constrained: true)
precondition(hint.constrained && !hint.expensive)

hint.accept(generation: current, state: .unavailable)
precondition(hint.state == .unavailable)

hint.accept(generation: current, state: .available)
precondition(hint.state == .available && !hint.constrained)

运行 xcrun swift NetworkStateCheck.swift,通过时没有输出,失败会以非零状态退出。它验证的是应用自己的状态契约,不是系统网络栈是否正确。

接入监听时固定执行顺序

生产适配器导入 Network,创建 NWPathMonitor(),先设置 pathUpdateHandler,再调用 start(queue:)。把三种 NWPath.Status 映射到模型枚举,同时传入 isExpensive 和 isConstrained;未知的未来枚举值映射为未知态。

创建监听时捕获当前代次。回调进入状态模型之前,再比较代次。重新监听时,先递增代次使旧事件失效,再取消旧实例,最后创建新实例。不要对已经取消的实例再次调用启动。

监听生命周期和模型更新应放在同一个串行执行域。若采用主执行域,回调切换过去以后再校验代次,否则排队中的旧事件仍可能污染新状态。向界面发布时也要保留这个顺序,不要让多个异步投递倒序覆盖。

不要仅用 requiredInterfaceType: .wifi 判断设备有没有网络:它观察的是特定接口条件,可能忽略其他可用路径。只有功能确实限定接口时才使用该初始化方式。

用事件矩阵补齐边界

把脚本迁入项目的单元测试目标,每个用例创建独立模型,至少补齐以下检查:

  • 启动没有回调:状态保持未知,请求仍可发出。
  • 路径可用但接口失败:只更新请求错误,不显示路径断开。
  • 路径不可用后恢复:横幅消失,不重复触发同一业务操作。
  • 取消后收到旧事件:状态和费用提示均不变化。
  • 同一事件重复到达:提示一致,不累计重试任务。
  • 高成本或受限状态变化:只调整业务允许的下载策略。

isExpensive 不等于某一种固定接口,isConstrained 也不表示完全断网。大文件任务可结合 URLSessionConfiguration 的费用与受限网络策略;不要仅凭提示模型静默丢弃用户操作。

分清自动化与设备验收

模拟器适合检查横幅、按钮和状态模型接线,但网络行为受宿主机影响。不要把“模拟器里的提示变了”当成蜂窝切换已经验收,更不要为测试关闭云端 Mac 的网络接口,导致远程桌面失联。

真机补测无线网络切换、受限网络和从后台返回,分别记录路径事件顺序、界面变化及请求结果。测试服务器拒绝连接与路径不可用是两类场景,应分开保留证据。

最后提交状态模型、事件用例和设备检查记录。验收标准不是横幅总能立刻猜中网络,而是提示不会误拦请求,旧监听不会覆盖新会话,失败后的用户操作有明确去向。

常见问题

NWPathMonitor 返回 satisfied,能否认定接口一定能访问?

不能。它表示存在满足监测条件的网络路径,不验证业务服务器、认证或响应内容。接口是否成功仍应由实际请求结果判定。

监听取消后,如何防止旧回调覆盖新状态?

在同一串行执行域中先递增监听代次,再取消旧监听;回调携带创建时的代次,状态模型只接收当前代次的事件。重新开始时创建新的监听实例。

用自己的任务,验证远程 Mac。

租用一台整机独享的 Mac mini M4,使用 macOS 图形界面与命令行。适合先验证连接和工作流;需要长期离线作业或外接专业硬件时,也应比较自购。

租用独享 Mac mini

先租一天,再决定要不要买。

把连接手感和实际任务一起验证,再选择适合自己的使用周期。