希紋的部落格

Swift-檢查網路狀態


參考原文: https://www.appcoda.com.tw/network-framework/

檢查網路環境的方法主要是import Network之後再建立NWPathMonitor物件去偵測手機的網路環境。

因此通常會推薦寫一個NetStatus.swift的Model去統一處理。

原始碼

import Foundation
//1 匯入 Network 框架
import Network

class NetStatus: NSObject {
    //MARK: - Singleton
    //2 單例模式
    static let shared = NetStatus()
    // MARK: - init
    private override init() {
        super.init()
    }
    
    deinit {
        stopMonitoring()
    }
    //MARK: - Values
    //Network 框架實際上是提供一個簡單的方式,透過 NWPathMonitor 類別來觀察網路變化。
    var monitor: NWPathMonitor?
    // 宣告一個旗標(Flag),來標示類別是否正在觀察網路狀態的變化
    // 預設值被設定為 false,因為沒有任何監控發生。
    var isMonitoring = false
    // callback values
    //宣告一些回呼處理器(閉包),在以下情況會呼叫它們:
    //類別開始監控網路變化。
    var didStartMonitoringHandler: (() -> Void)?
    //類別停止監控網路變化。
    var didStopMonitoringHandler: (() -> Void)?
    //網路狀態出現變更。
    var netStatusChangeHandler: (() -> Void)?
    // 確認連接狀態
    var isConnected: Bool {
        //當 monitor 是 nil 的時候,我們並沒有辦法判定裝置是否有連線。
        guard let monitor = monitor else { return false }
        //如果數值等於 satisfied,就代表裝置已連線到一個網路介面。
        return monitor.currentPath.status == .satisfied
    }
    // 取出當前連線介面(wifi,Cellular等)
    var interfaceType: NWInterface.InterfaceType? {
        guard let monitor = monitor else { return nil }

        return monitor.currentPath.availableInterfaces.first?.type
    }
    // 啟用的所有連線介面
    var availableInterfacesTypes: [NWInterface.InterfaceType]? {
        guard let monitor = monitor else { return nil }
        return monitor.currentPath.availableInterfaces.map { $0.type }
    }
    /**
     根據 Apple 官方文件,特定的網路介面(像是行動網路)被認為是昂貴的。
     Network 框架有一個旗標讓它可以簡單地透過 currentPath 屬性確認這一點,它就叫做 isExpensive。
     */
    var isExpensive: Bool {
        return monitor?.currentPath.isExpensive ?? false
    }
    /**
     ChatGPT:
     NWPathMonitor.currentPath.isExpensive 是 Network API 的屬性,用來判斷目前的網路是否為昂貴網路(例如蜂窩網路或按流量計費的網路)。然而,有可能在某些情況下,即使是 5G 網路,其值也可能是 false,以下是可能的原因與解釋:

     1. 運營商的配置
     並非所有的蜂窩網路(包括 5G)都會被標記為「昂貴網路」。這取決於運營商的網路設置,例如某些 5G 計劃可能被視為「不限流量」,因此不會被歸類為昂貴網路。
     2. iOS 的判定邏輯
     iOS 的 isExpensive 是基於系統的內部判定邏輯來決定的。Apple 可能根據蜂窩網路的使用模式(如是否開啟低數據模式或計費模式)來設定這個屬性,而不僅僅是根據網路類型(如 5G)。
     3. Wi-Fi 優先接入
     如果設備當前同時連接到 Wi-Fi 和 5G,並且優先使用 Wi-Fi,那麼即使有 5G 連接,isExpensive 也可能是 false,因為當前的主要網路是 Wi-Fi。
     4. NWPathMonitor 問題
     NWPathMonitor 提供的 currentPath 可能有短暫的延遲,無法立即反映網路狀態的變化,特別是在從 Wi-Fi 切換到 5G 的過程中。
     */
    var isCellular: Bool {
        guard let type = interfaceType else { return false }
        //如果只是單純判定是否為行動網路,只要選蜂窩(cellular)網路的技術(例如 LTE、NR 等)
        //萬一要細分到是否為4G、5G就要透過CoreTelephony來做進一步分析
        return type == .cellular
    }
    //將interfaceType轉成文字,原因在於這個值並沒有文字輸出
    var typeString: String {
        guard let type = interfaceType else { return "None" }
        switch type {
        case .cellular:
            return "Cellular"
        case .loopback:
            return "Loopback"
        case .other:
            return "Other"
        case .wifi:
            return "WiFi"
        case .wiredEthernet:
            return "WiredEhernet"
        default:
            return "Unknown"
        }
    }
    //MARK: - Functions
    // 開始監控網路狀態變化
    func startMonitoring() {
        guard !isMonitoring else { return }
        // 建立物件
        monitor = NWPathMonitor()
        /**
         很重要的一點,就是觀察網路狀態變化需要在背景執行緒中執行,也不可以在主執行緒中執行。
         所以,讓我們來建立一個 DispatchQueue,然後呼叫方法
         */
        let queue = DispatchQueue(label: "NetStatus_Monitor")
        monitor?.start(queue: queue)
        /**
         讓 NetStatus 在任何網路狀態變化時通知它的呼叫器,像是從無線網路切換至行動網路,或是完全斷線的時候。這個步驟是透過 pathUpdateHandler 屬性來完成的,它是一個閉包,會回傳含有關於更新網路介面、裝置有否連上網路等資訊的 NWPath 物件。不過,我們在這裡不會直接使用它,反而我們會呼叫前文宣告的 netStatusChangeHandler
         */
        monitor?.pathUpdateHandler = { (path: NWPath) in
            self.netStatusChangeHandler?()
        }
        //  最後,按需要更新 isMonitoring 旗標,並呼叫 didStartMonitoringHandler 閉包
        isMonitoring = true
        didStartMonitoringHandler?()
    }
    // 停止監控
    func stopMonitoring() {
        guard isMonitoring, let monitor = monitor else { return }
        //要停止監控,我們只需要呼叫下列的 cancel() 方法
        monitor.cancel()
        //之後,也要更新我們客製化的屬性
        self.monitor = nil
        isMonitoring = false
        //我們亦要呼叫 didStopMonitoringHandler,來通知使用這個類別的任何實體
        didStopMonitoringHandler?()
    }
}

使用方法

以APP使用為舉例

import UIKit

class ViewController: UIViewController {
    //MARK: - @IBOutlet
    @IBOutlet weak var txvInfo: UITextView!
    @IBOutlet weak var btnMonitor: UIButton!
    
    //MARK: - Values
    let netStatus = NetStatus.shared
    //MARK: - lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        txvInfo.text = ""
        //事件觸發設置
        netStatus.didStartMonitoringHandler = handlerStartMonitoring
        netStatus.didStopMonitoringHandler = handlerStopMonitoring
        netStatus.netStatusChangeHandler = handlerNetWorkChange
    }
    //MARK: - Functions
    //Handlers事件
    func handlerStartMonitoring() {
        outputContent("Handler:Start Monitoring")
        outputContent("NteType: \(netStatus.typeString)")
    }
    func handlerStopMonitoring() {
        outputContent("Handler:Stop Monitoring")
    }
    func handlerNetWorkChange() {
        outputContent("Handler:NetWork Chnage")
        outputContent("NteType: \(netStatus.typeString)")//印出現在使用的網路
    }
    //輸出文字
    func outputContent(_ str: String) {
        DispatchQueue.main.async {
            self.txvInfo.text.append(str + "\n")
        }
    }
    //MARK: - @IBAction
    @IBAction func doMonitoring(_ sender: Any) {
        var btnTitle = "START Monitor"
        
        if netStatus.isMonitoring {
            netStatus.stopMonitoring()//開始偵測
        }
        else {
            netStatus.startMonitoring()//停止偵測
            btnTitle = "STOP Monitor"
        }
        
        btnMonitor.setTitle(btnTitle, for: .normal)//按鈕名稱更換
    }
    
    @IBAction func doCheckNet(_ sender: Any) {
        //在沒有監控網路的時候,isConnected的結果一定是False
        if netStatus.isMonitoring {
            let showText = netStatus.isConnected ? "Connected" : "Not Connected"
            outputContent(showText)
        }
        else {
            outputContent("Monitor not start.")
        }
    }
    
}

結語

想說是參考原文之後整理出來的,但是要模仿原文寫一遍又很麻煩,所以直接把程式碼貼上去,上面的註解是我在閱讀時的筆記,應用上就沒太多註解。

#swift