medmap-ios/medmap/Utilities/NetworkMonitor.swift
2026-08-27 23:26:14 +01:00

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