125 lines
3.3 KiB
Swift
125 lines
3.3 KiB
Swift
//
|
|
// NetworkMonitor.swift
|
|
// FluxRead
|
|
//
|
|
// Created by RSA on 29/03/2026.
|
|
//
|
|
|
|
import Foundation
|
|
import Network
|
|
import Combine
|
|
|
|
// MARK: - Connection Type
|
|
|
|
enum NetworkConnectionType {
|
|
case wifi
|
|
case cellular
|
|
case ethernet
|
|
case none
|
|
case unknown
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .wifi: return "Wi-Fi"
|
|
case .cellular: return "Cellular"
|
|
case .ethernet: return "Ethernet"
|
|
case .none: return "No Connection"
|
|
case .unknown: return "Unknown"
|
|
}
|
|
}
|
|
|
|
/// True for any connection that is generally considered unmetered / fast.
|
|
var isWifi: Bool { self == .wifi || self == .ethernet }
|
|
|
|
/// True only when the device is on a metered cellular connection.
|
|
var isCellular: Bool { self == .cellular }
|
|
|
|
/// True when any network path is available.
|
|
var isConnected: Bool { self != .none }
|
|
}
|
|
|
|
// MARK: - NetworkMonitor
|
|
|
|
/// Observes the device's active network path and exposes the current
|
|
/// connection type as both a `@Published` property (for SwiftUI / Combine)
|
|
/// and a synchronous getter.
|
|
///
|
|
/// Usage:
|
|
/// ```swift
|
|
/// // Singleton
|
|
/// let type = NetworkMonitor.shared.connectionType
|
|
///
|
|
/// // Injected instance (via DI)
|
|
/// monitor.$connectionType
|
|
/// .sink { print($0.description) }
|
|
/// .store(in: &cancellables)
|
|
/// ```
|
|
final class NetworkMonitor: ObservableObject {
|
|
|
|
// MARK: Singleton
|
|
|
|
static let shared = NetworkMonitor()
|
|
|
|
// MARK: Published State
|
|
|
|
/// The current network connection type. Updated on the main queue.
|
|
@Published private(set) var connectionType: NetworkConnectionType = .unknown
|
|
|
|
/// Convenience: `true` when any usable network path exists.
|
|
@Published private(set) var isConnected: Bool = false
|
|
|
|
// MARK: Private
|
|
|
|
private let monitor: NWPathMonitor
|
|
private let monitorQueue = DispatchQueue(label: "com.fluxread.networkmonitor", qos: .utility)
|
|
|
|
// MARK: Init
|
|
|
|
init() {
|
|
monitor = NWPathMonitor()
|
|
start()
|
|
}
|
|
|
|
deinit {
|
|
monitor.cancel()
|
|
}
|
|
|
|
// MARK: Public API
|
|
|
|
/// Synchronously returns the current connection type based on the
|
|
/// latest path snapshot. Always safe to call from any thread.
|
|
var currentConnectionType: NetworkConnectionType {
|
|
return connectionType
|
|
}
|
|
|
|
/// Returns `true` when the active interface matches a given type.
|
|
func isUsing(_ interface: NWInterface.InterfaceType) -> Bool {
|
|
monitor.currentPath.usesInterfaceType(interface)
|
|
}
|
|
|
|
// MARK: Private
|
|
|
|
private func start() {
|
|
monitor.pathUpdateHandler = { [weak self] path in
|
|
guard let self else { return }
|
|
let type = Self.resolve(path: path)
|
|
DispatchQueue.main.async {
|
|
self.connectionType = type
|
|
self.isConnected = path.status == .satisfied
|
|
}
|
|
}
|
|
monitor.start(queue: monitorQueue)
|
|
}
|
|
|
|
/// Maps an `NWPath` to a `NetworkConnectionType`.
|
|
private static func resolve(path: NWPath) -> NetworkConnectionType {
|
|
guard path.status == .satisfied else { return .none }
|
|
|
|
if path.usesInterfaceType(.wifi) { return .wifi }
|
|
if path.usesInterfaceType(.cellular) { return .cellular }
|
|
if path.usesInterfaceType(.wiredEthernet) { return .ethernet }
|
|
|
|
return .unknown
|
|
}
|
|
}
|