NetworkMonitor.swift 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. //
  2. // NetworkMonitor.swift
  3. // Kingfisher
  4. //
  5. // Created by Vladislav Komkov on 2025/09/22.
  6. //
  7. // Copyright (c) 2020 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import Network
  27. import Foundation
  28. /// A protocol for network connectivity monitoring that allows for dependency injection and testing.
  29. public protocol NetworkMonitoring: Sendable {
  30. /// Whether the network is currently connected.
  31. var isConnected: Bool { get }
  32. /// Observes network connectivity changes with an optional timeout.
  33. /// - Parameters:
  34. /// - timeoutInterval: The timeout for waiting for network reconnection. If nil, no timeout is applied.
  35. /// - callback: The callback to be called when network state changes or timeout occurs.
  36. /// - Returns: A cancellable observer that can be used to cancel the observation.
  37. func observeConnectivity(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void) -> NetworkObserver
  38. }
  39. /// A protocol for network observers that can be cancelled.
  40. public protocol NetworkObserver: Sendable {
  41. /// Cancels the network observation.
  42. func cancel()
  43. }
  44. /// A shared singleton that manages network connectivity monitoring.
  45. /// This prevents creating multiple NWPathMonitor instances when many NetworkRetryStrategy instances are used.
  46. /// The monitor is created lazily only when first accessed.
  47. public final class NetworkMonitor: @unchecked Sendable, NetworkMonitoring {
  48. public static let `default` = NetworkMonitor()
  49. /// Whether the network is currently connected.
  50. public var isConnected: Bool {
  51. return monitor.currentPath.status == .satisfied
  52. }
  53. /// The network path monitor for observing connectivity changes.
  54. private let monitor = NWPathMonitor()
  55. /// The queue for monitoring network changes.
  56. private let monitorQueue = DispatchQueue(label: "com.onevcat.Kingfisher.NetworkMonitor", qos: .utility)
  57. /// Observers waiting for network reconnection.
  58. private var observers: [NetworkObserverImpl] = []
  59. private let observersQueue = DispatchQueue(label: "com.onevcat.Kingfisher.NetworkMonitor.Observers", attributes: .concurrent)
  60. /// Whether the monitor has been started.
  61. private var isStarted = false
  62. private let startQueue = DispatchQueue(label: "com.onevcat.Kingfisher.NetworkMonitor.Start")
  63. private init() {
  64. // Set up path monitoring
  65. monitor.pathUpdateHandler = { [weak self] path in
  66. self?.handlePathUpdate(path)
  67. }
  68. }
  69. /// Starts monitoring if not already started.
  70. private func startMonitoring() {
  71. startQueue.sync {
  72. guard !isStarted else { return }
  73. monitor.start(queue: monitorQueue)
  74. isStarted = true
  75. }
  76. }
  77. /// Handles network path updates and notifies observers.
  78. private func handlePathUpdate(_ path: NWPath) {
  79. let connected = path.status == .satisfied
  80. guard connected else { return }
  81. // Notify all observers that network is available
  82. observersQueue.async(flags: .barrier) {
  83. let activeObservers = self.observers
  84. self.observers.removeAll()
  85. DispatchQueue.main.async {
  86. activeObservers.forEach { $0.notify(isConnected: true) }
  87. }
  88. }
  89. }
  90. /// Adds an observer for network reconnection.
  91. private func addObserver(_ observer: NetworkObserverImpl) {
  92. startMonitoring()
  93. observersQueue.async(flags: .barrier) {
  94. self.observers.append(observer)
  95. }
  96. }
  97. /// Removes an observer.
  98. internal func removeObserver(_ observer: NetworkObserverImpl) {
  99. observersQueue.async(flags: .barrier) {
  100. self.observers.removeAll { $0 === observer }
  101. }
  102. }
  103. // MARK: - NetworkMonitoring
  104. public func observeConnectivity(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void) -> NetworkObserver {
  105. let observer = NetworkObserverImpl(
  106. timeoutInterval: timeoutInterval,
  107. callback: callback,
  108. monitor: self
  109. )
  110. addObserver(observer)
  111. return observer
  112. }
  113. }
  114. /// Internal implementation of network observer that manages timeout and callbacks.
  115. internal final class NetworkObserverImpl: @unchecked Sendable, NetworkObserver {
  116. let timeoutInterval: TimeInterval?
  117. let callback: @Sendable (Bool) -> Void
  118. private weak var monitor: NetworkMonitor?
  119. private var timeoutWorkItem: DispatchWorkItem?
  120. private let queue = DispatchQueue(label: "com.onevcat.Kingfisher.NetworkObserver", qos: .utility)
  121. init(timeoutInterval: TimeInterval?, callback: @escaping @Sendable (Bool) -> Void, monitor: NetworkMonitor) {
  122. self.timeoutInterval = timeoutInterval
  123. self.callback = callback
  124. self.monitor = monitor
  125. // Set up timeout if specified
  126. if let timeoutInterval = timeoutInterval {
  127. let workItem = DispatchWorkItem { [weak self] in
  128. self?.notify(isConnected: false)
  129. }
  130. timeoutWorkItem = workItem
  131. queue.asyncAfter(deadline: .now() + timeoutInterval, execute: workItem)
  132. }
  133. }
  134. func notify(isConnected: Bool) {
  135. queue.async { [weak self] in
  136. guard let self else { return }
  137. // Cancel timeout if we're notifying
  138. timeoutWorkItem?.cancel()
  139. timeoutWorkItem = nil
  140. // Remove from monitor
  141. monitor?.removeObserver(self)
  142. // Call the callback
  143. DispatchQueue.main.async {
  144. self.callback(isConnected)
  145. }
  146. }
  147. }
  148. public func cancel() {
  149. queue.async { [weak self] in
  150. guard let self else { return }
  151. // Cancel timeout
  152. timeoutWorkItem?.cancel()
  153. timeoutWorkItem = nil
  154. // Remove from monitor
  155. monitor?.removeObserver(self)
  156. }
  157. }
  158. }