Skip to content

Commit 060da04

Browse files
committed
Add Network Monitoring Feature
1 parent a49242d commit 060da04

File tree

3 files changed

+289
-1
lines changed

3 files changed

+289
-1
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import Foundation
2+
import Network
3+
4+
/**
5+
* Network connection type.
6+
*/
7+
public enum ConnectionType: String, Sendable {
8+
case wifi = "WiFi"
9+
case cellular = "Cellular"
10+
case ethernet = "Ethernet"
11+
case loopback = "Loopback"
12+
case other = "Other"
13+
case unavailable = "Unavailable"
14+
}
15+
16+
/**
17+
* Network connection status.
18+
*/
19+
public enum NetworkStatus: Sendable {
20+
case connected(ConnectionType)
21+
case disconnected
22+
case connecting
23+
case requiresConnection
24+
}
25+
26+
/**
27+
* A protocol for monitoring network connectivity.
28+
*
29+
* NetworkMonitor provides real-time information about the device's network
30+
* connection status, including connection type (WiFi, Cellular, etc.) and
31+
* availability. This allows your app to adapt its behavior based on network conditions.
32+
*
33+
* ## Usage
34+
* ```swift
35+
* let monitor = DefaultNetworkMonitor()
36+
*
37+
* // Check current status
38+
* let status = await monitor.currentStatus
39+
* if case .connected(let type) = status {
40+
* print("Connected via \(type)")
41+
* }
42+
*
43+
* // Observe status changes
44+
* for await status in monitor.statusUpdates {
45+
* switch status {
46+
* case .connected(let type):
47+
* print("Connected via \(type)")
48+
* case .disconnected:
49+
* print("No connection")
50+
* default:
51+
* break
52+
* }
53+
* }
54+
* ```
55+
*/
56+
public protocol NetworkMonitor: Sendable {
57+
/// The current network status
58+
var currentStatus: NetworkStatus { get async }
59+
60+
/// An async sequence of network status updates
61+
var statusUpdates: AsyncStream<NetworkStatus> { get }
62+
63+
/// Whether the device is currently connected to the internet
64+
var isConnected: Bool { get async }
65+
66+
/// The current connection type, if connected
67+
var connectionType: ConnectionType? { get async }
68+
69+
/// Starts monitoring network connectivity
70+
func startMonitoring()
71+
72+
/// Stops monitoring network connectivity
73+
func stopMonitoring()
74+
}
75+
76+
/**
77+
* Default implementation of NetworkMonitor using Network Framework.
78+
*
79+
* This monitor uses NWPathMonitor to track network connectivity and provides
80+
* real-time updates about connection status and type.
81+
*/
82+
public class DefaultNetworkMonitor: NetworkMonitor, @unchecked Sendable {
83+
private let monitor: NWPathMonitor
84+
private let queue: DispatchQueue
85+
private var statusContinuation: AsyncStream<NetworkStatus>.Continuation?
86+
private var currentStatusValue: NetworkStatus = .disconnected
87+
88+
public init(requiredInterfaceType: NWInterface.InterfaceType? = nil) {
89+
if let interfaceType = requiredInterfaceType {
90+
self.monitor = NWPathMonitor(requiredInterfaceType: interfaceType)
91+
} else {
92+
self.monitor = NWPathMonitor()
93+
}
94+
self.queue = DispatchQueue(label: "com.networkkit.monitor")
95+
}
96+
97+
public var currentStatus: NetworkStatus {
98+
get async {
99+
return await withCheckedContinuation { continuation in
100+
queue.async {
101+
continuation.resume(returning: self.currentStatusValue)
102+
}
103+
}
104+
}
105+
}
106+
107+
public var statusUpdates: AsyncStream<NetworkStatus> {
108+
AsyncStream { continuation in
109+
statusContinuation = continuation
110+
111+
monitor.pathUpdateHandler = { [weak self] path in
112+
guard let self = self else { return }
113+
114+
let status = self.status(from: path)
115+
self.currentStatusValue = status
116+
117+
self.queue.async {
118+
continuation.yield(status)
119+
}
120+
}
121+
122+
continuation.onTermination = { [weak self] _ in
123+
self?.stopMonitoring()
124+
}
125+
}
126+
}
127+
128+
public var isConnected: Bool {
129+
get async {
130+
let status = await currentStatus
131+
if case .connected = status {
132+
return true
133+
}
134+
return false
135+
}
136+
}
137+
138+
public var connectionType: ConnectionType? {
139+
get async {
140+
let status = await currentStatus
141+
if case .connected(let type) = status {
142+
return type
143+
}
144+
return nil
145+
}
146+
}
147+
148+
public func startMonitoring() {
149+
monitor.start(queue: queue)
150+
}
151+
152+
public func stopMonitoring() {
153+
monitor.cancel()
154+
statusContinuation?.finish()
155+
statusContinuation = nil
156+
}
157+
158+
private func status(from path: NWPath) -> NetworkStatus {
159+
guard path.status == .satisfied else {
160+
if path.status == .requiresConnection {
161+
return .requiresConnection
162+
}
163+
return .disconnected
164+
}
165+
166+
// Determine connection type
167+
let connectionType: ConnectionType
168+
169+
if path.usesInterfaceType(.wifi) {
170+
connectionType = .wifi
171+
} else if path.usesInterfaceType(.cellular) {
172+
connectionType = .cellular
173+
} else if path.usesInterfaceType(.wiredEthernet) {
174+
connectionType = .ethernet
175+
} else if path.usesInterfaceType(.loopback) {
176+
connectionType = .loopback
177+
} else {
178+
connectionType = .other
179+
}
180+
181+
return .connected(connectionType)
182+
}
183+
}
184+
185+
/**
186+
* A network monitor that only checks WiFi connectivity.
187+
*/
188+
public final class WiFiNetworkMonitor: DefaultNetworkMonitor, @unchecked Sendable {
189+
public init() {
190+
super.init(requiredInterfaceType: .wifi)
191+
}
192+
}
193+
194+
/**
195+
* A network monitor that only checks cellular connectivity.
196+
*/
197+
public final class CellularNetworkMonitor: DefaultNetworkMonitor, @unchecked Sendable {
198+
public init() {
199+
super.init(requiredInterfaceType: .cellular)
200+
}
201+
}

Sources/NetworkKit/Core/NetworkProvider.swift

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ public final class NetworkProvider<E: Endpoint> {
5656
/// Cache manager for response caching
5757
private let cacheManager: CacheManager?
5858

59+
/// Network monitor for connectivity checking (iOS 12.0+, macOS 10.14+)
60+
private var networkMonitor: (any NetworkMonitor)?
61+
62+
/// Whether to check network connectivity before making requests
63+
private let checkConnectivity: Bool
64+
5965
/**
6066
* Initializes a new NetworkProvider with the specified configuration.
6167
*
@@ -70,6 +76,8 @@ public final class NetworkProvider<E: Endpoint> {
7076
* - rateLimiter: Limiter for request rate control. Defaults to `nil`.
7177
* - circuitBreaker: Circuit breaker for fault tolerance. Defaults to `nil`.
7278
* - cacheManager: Manager for response caching. Defaults to `nil`.
79+
* - networkMonitor: Network monitor for connectivity checking. Defaults to `nil`.
80+
* - checkConnectivity: Whether to check network connectivity before requests. Defaults to `false`.
7381
*/
7482
public init(session: Session = URLSession.shared,
7583
plugins: [NetworkPlugin] = [],
@@ -81,7 +89,9 @@ public final class NetworkProvider<E: Endpoint> {
8189
securityManager: SecurityManager? = nil,
8290
rateLimiter: RateLimiter? = nil,
8391
circuitBreaker: CircuitBreaker? = nil,
84-
cacheManager: CacheManager? = nil) {
92+
cacheManager: CacheManager? = nil,
93+
checkConnectivity: Bool = false,
94+
networkMonitor: (any NetworkMonitor)? = nil) {
8595
self.session = session
8696
self.plugins = plugins
8797
self.responseHandler = responseHandler
@@ -92,6 +102,15 @@ public final class NetworkProvider<E: Endpoint> {
92102
self.rateLimiter = rateLimiter
93103
self.circuitBreaker = circuitBreaker
94104
self.cacheManager = cacheManager
105+
self.checkConnectivity = checkConnectivity
106+
107+
if let monitor = networkMonitor {
108+
self.networkMonitor = monitor
109+
monitor.startMonitoring()
110+
} else if checkConnectivity {
111+
self.networkMonitor = DefaultNetworkMonitor()
112+
(self.networkMonitor as? DefaultNetworkMonitor)?.startMonitoring()
113+
}
95114
}
96115

97116
/**
@@ -121,6 +140,30 @@ public final class NetworkProvider<E: Endpoint> {
121140
* - Throws: `NetworkError` if the request fails
122141
*/
123142
public func request<T: Decodable>(_ endpoint: E, as type: T.Type, modifiers: [RequestModifier] = []) async throws -> T {
143+
// Check network connectivity if enabled
144+
if checkConnectivity {
145+
if let monitor = networkMonitor {
146+
let isConnected = await monitor.isConnected
147+
if !isConnected {
148+
// If cache is available, try to return cached data
149+
if let cacheManager = cacheManager {
150+
let cacheKey = generateCacheKey(for: endpoint)
151+
if let cachedData: Data = cacheManager.get(for: cacheKey, as: Data.self) {
152+
do {
153+
let decoder = JSONDecoder()
154+
let cachedResult = try decoder.decode(T.self, from: cachedData)
155+
metricsCollector?.recordCacheHit(for: cacheKey)
156+
return cachedResult
157+
} catch {
158+
// Cache decode failed, throw no connection error
159+
}
160+
}
161+
}
162+
throw NetworkError.noConnection
163+
}
164+
}
165+
}
166+
124167
// Check circuit breaker
125168
if let circuitBreaker = circuitBreaker, !circuitBreaker.shouldAllowRequest() {
126169
throw NetworkError.circuitBreakerOpen
@@ -338,6 +381,44 @@ public final class NetworkProvider<E: Endpoint> {
338381
return (metricsCollector as? DefaultMetricsCollector)?.getMetrics()
339382
}
340383

384+
/**
385+
* Gets the current network connection status.
386+
*
387+
* Returns the current network status if connectivity checking is enabled.
388+
*
389+
* - Returns: Current network status, or nil if connectivity checking is not enabled
390+
*/
391+
public func getNetworkStatus() async -> NetworkStatus? {
392+
guard let monitor = networkMonitor else {
393+
return nil
394+
}
395+
return await monitor.currentStatus
396+
}
397+
398+
/**
399+
* Checks if the device is currently connected to the internet.
400+
*
401+
* - Returns: true if connected, false if not connected, or nil if connectivity checking is not enabled
402+
*/
403+
public func isConnected() async -> Bool? {
404+
guard let monitor = networkMonitor else {
405+
return nil
406+
}
407+
return await monitor.isConnected
408+
}
409+
410+
/**
411+
* Gets the current connection type.
412+
*
413+
* - Returns: The connection type (WiFi, Cellular, etc.), or nil if not connected or monitoring is not enabled
414+
*/
415+
public func getConnectionType() async -> ConnectionType? {
416+
guard let monitor = networkMonitor else {
417+
return nil
418+
}
419+
return await monitor.connectionType
420+
}
421+
341422
/**
342423
* Resets all network metrics to zero.
343424
*

Sources/NetworkKit/Response/NetworkError.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public enum NetworkError: LocalizedError, Equatable {
4545
/// An error occurred during caching operations
4646
case cachingError(Error)
4747

48+
/// Network connection is not available
49+
case noConnection
50+
4851
/// An unknown error occurred
4952
case unknown
5053

@@ -66,6 +69,8 @@ public enum NetworkError: LocalizedError, Equatable {
6669
return "Rate limit exceeded"
6770
case .cachingError(let error):
6871
return "Caching error: \(error.localizedDescription)"
72+
case .noConnection:
73+
return "Network connection is not available"
6974
case .unknown:
7075
return "Unknown error occurred"
7176
}
@@ -80,6 +85,7 @@ public enum NetworkError: LocalizedError, Equatable {
8085
(.circuitBreakerOpen, .circuitBreakerOpen),
8186
(.rateLimitExceeded, .rateLimitExceeded),
8287
(.cachingError, .cachingError),
88+
(.noConnection, .noConnection),
8389
(.unknown, .unknown):
8490
return true
8591
default:

0 commit comments

Comments
 (0)