雲端 Mac 的 iOS 網路狀態提示驗收:NWPathMonitor 與離線判定邊界
使用者點選重新整理時,畫面顯示「網路無法使用」,稍後重試卻成功了。常見原因不是請求層失效,而是應用程式把尚未更新的網路路徑狀態當成了能否發送請求的判斷依據。在雲端 Mac 上開發 iOS 應用程式時,可以先用可重現的事件序列驗收狀態模型,再透過模擬器與實機檢查系統行為,不必為了製造故障而反覆中斷遠端連線。
先將提示與請求結果分開
NWPathMonitor 觀察的是符合監測條件的網路路徑。.satisfied 不保證伺服器在線,也不保證身分驗證有效;.requiresConnection 表示需要建立連線,不能直接解讀為持續離線。
產品層至少應保留「未知、路徑可用、需要連線、路徑不可用」四種狀態。初次啟動、尚未收到回呼時,應顯示未知,不要預設為離線。API 請求逾時、身分驗證失敗和伺服器錯誤應分別納入請求結果模型,不要反過來覆寫路徑狀態。
網路狀態適合用來提供提示,不適合作為「禁止發送所有請求」的總開關。使用者主動重試時,應由實際請求驗證能否連線。
這套做法適合需要網路狀態橫幅、自動重試提示的應用程式;如果功能只是發送一次請求,直接處理請求結果通常更簡單,不必額外加入監聽。
建立可獨立執行的狀態模型
在遠端 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 判斷裝置是否有網路:它觀察的是特定介面條件,可能忽略其他可用路徑。只有功能確實限定使用特定介面時,才採用這種初始化方式。
用事件矩陣補齊邊界情境
將腳本移入專案的單元測試目標,每個測試案例都建立獨立模型,至少補齊以下檢查:
- 啟動後沒有回呼:狀態保持未知,仍可發送請求。
- 路徑可用但 API 請求失敗:只更新請求錯誤,不顯示路徑中斷。
- 路徑不可用後恢復:橫幅消失,不重複觸發同一項業務操作。
- 取消後收到舊事件:狀態與網路成本提示都不變。
- 同一事件重複抵達:提示維持一致,不累積重試工作。
- 高成本或受限狀態改變:只調整業務規則允許的下載策略。
isExpensive 不等於某一種固定介面,isConstrained 也不代表完全斷網。大型檔案工作可搭配 URLSessionConfiguration 的網路成本與受限網路策略;不要只憑提示模型就默默捨棄使用者操作。
釐清自動化測試與裝置驗收的分工
模擬器適合檢查橫幅、按鈕和狀態模型的串接,但網路行為會受到主機影響。不要把「模擬器裡的提示變了」當成行動網路切換已通過驗收,更不要為了測試而關閉雲端 Mac 的網路介面,導致遠端桌面失聯。
使用實機補測無線網路切換、受限網路,以及從背景返回前景的情境,分別記錄路徑事件順序、介面變化與請求結果。測試伺服器拒絕連線和路徑不可用是兩種不同情境,應分別保留證據。
最後提交狀態模型、事件測試案例與裝置檢查紀錄。驗收標準不是橫幅總能立即猜中網路狀態,而是提示不會誤擋請求、舊監聽不會覆寫新工作階段的狀態,而且失敗後的使用者操作都有明確的處理方式。
常見問題
NWPathMonitor 回傳 satisfied,代表介面一定可連線嗎?
不代表。它只表示有符合監測條件的網路路徑,不驗證伺服器、認證或回應內容。介面是否成功仍須依實際請求結果判定。
取消監聽後,如何避免舊回呼覆寫新狀態?
在同一個序列執行範圍內先增加監聽世代,再取消舊監聽。回呼攜帶建立時的世代,模型只接受目前世代的事件;重新開始時建立新的監聽實例。
用實際工作,測試遠端 Mac。
租用一台專屬使用的 Mac mini M4,透過 macOS 圖形介面與命令列操作。適合先測試連線和工作流程;若需要長時間離線運作或連接專業硬體,也可以評估自行購買。
租用專屬 Mac mini