NetworkReachabilityManager.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. //
  2. // NetworkReachabilityManager.swift
  3. //
  4. // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. #if canImport(SystemConfiguration)
  25. import Foundation
  26. import SystemConfiguration
  27. /// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both cellular and
  28. /// WiFi network interfaces.
  29. ///
  30. /// Reachability can be used to determine background information about why a network operation failed, or to retry
  31. /// network requests when a connection is established. It should not be used to prevent a user from initiating a network
  32. /// request, as it's possible that an initial request may be required to establish reachability.
  33. @available(macOS, deprecated: 14.4, message: "Use NWPathMonitor instead.")
  34. @available(iOS, deprecated: 17.4, message: "Use NWPathMonitor instead.")
  35. @available(watchOS, deprecated: 9.4, message: "Use NWPathMonitor instead.")
  36. @available(tvOS, deprecated: 17.4, message: "Use NWPathMonitor instead.")
  37. @available(visionOS, deprecated: 1.4, message: "Use NWPathMonitor instead.")
  38. open class NetworkReachabilityManager: @unchecked Sendable {
  39. /// Defines the various states of network reachability.
  40. public enum NetworkReachabilityStatus: Equatable, Sendable {
  41. /// It is unknown whether the network is reachable.
  42. case unknown
  43. /// The network is not reachable.
  44. case notReachable
  45. /// The network is reachable on the associated `ConnectionType`.
  46. case reachable(ConnectionType)
  47. init(_ flags: SCNetworkReachabilityFlags) {
  48. guard flags.isActuallyReachable else { self = .notReachable; return }
  49. var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)
  50. if flags.isCellular { networkStatus = .reachable(.cellular) }
  51. self = networkStatus
  52. }
  53. /// Defines the various connection types detected by reachability flags.
  54. public enum ConnectionType: Sendable {
  55. /// The connection type is either over Ethernet or WiFi.
  56. case ethernetOrWiFi
  57. /// The connection type is a cellular connection.
  58. case cellular
  59. }
  60. }
  61. /// A closure executed when the network reachability status changes. The closure takes a single argument: the
  62. /// network reachability status.
  63. public typealias Listener = @Sendable (NetworkReachabilityStatus) -> Void
  64. /// Default `NetworkReachabilityManager` for the zero address and a `listenerQueue` of `.main`.
  65. public static let `default` = NetworkReachabilityManager()
  66. // MARK: - Properties
  67. /// Whether the network is currently reachable.
  68. open var isReachable: Bool { isReachableOnCellular || isReachableOnEthernetOrWiFi }
  69. /// Whether the network is currently reachable over the cellular interface.
  70. ///
  71. /// - Note: Using this property to decide whether to make a high or low bandwidth request is not recommended.
  72. /// Instead, set the `allowsCellularAccess` on any `URLRequest`s being issued.
  73. ///
  74. open var isReachableOnCellular: Bool { status == .reachable(.cellular) }
  75. /// Whether the network is currently reachable over Ethernet or WiFi interface.
  76. open var isReachableOnEthernetOrWiFi: Bool { status == .reachable(.ethernetOrWiFi) }
  77. /// `DispatchQueue` on which reachability will update.
  78. public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue")
  79. /// Flags of the current reachability type, if any.
  80. open var flags: SCNetworkReachabilityFlags? {
  81. var flags = SCNetworkReachabilityFlags()
  82. return SCNetworkReachabilityGetFlags(reachability, &flags) ? flags : nil
  83. }
  84. /// The current network reachability status.
  85. open var status: NetworkReachabilityStatus {
  86. flags.map(NetworkReachabilityStatus.init) ?? .unknown
  87. }
  88. /// Mutable state storage.
  89. struct MutableState {
  90. /// A closure executed when the network reachability status changes.
  91. var listener: Listener?
  92. /// `DispatchQueue` on which listeners will be called.
  93. var listenerQueue: DispatchQueue?
  94. /// Previously calculated status.
  95. var previousStatus: NetworkReachabilityStatus?
  96. }
  97. /// `SCNetworkReachability` instance providing notifications.
  98. private let reachability: SCNetworkReachability
  99. /// Protected storage for mutable state.
  100. private let mutableState = Protected(MutableState())
  101. // MARK: - Initialization
  102. /// Creates an instance with the specified host.
  103. ///
  104. /// - Note: The `host` value must *not* contain a scheme, just the hostname.
  105. ///
  106. /// - Parameters:
  107. /// - host: Host used to evaluate network reachability. Must *not* include the scheme (e.g. `https`).
  108. public convenience init?(host: String) {
  109. guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
  110. self.init(reachability: reachability)
  111. }
  112. /// Creates an instance that monitors the address 0.0.0.0.
  113. ///
  114. /// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing
  115. /// status of the device, both IPv4 and IPv6.
  116. public convenience init?() {
  117. var zero = sockaddr()
  118. zero.sa_len = UInt8(MemoryLayout<sockaddr>.size)
  119. zero.sa_family = sa_family_t(AF_INET)
  120. guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil }
  121. self.init(reachability: reachability)
  122. }
  123. private init(reachability: SCNetworkReachability) {
  124. self.reachability = reachability
  125. }
  126. deinit {
  127. stopListening()
  128. }
  129. // MARK: - Listening
  130. /// Starts listening for changes in network reachability status.
  131. ///
  132. /// - Note: Stops and removes any existing listener.
  133. ///
  134. /// - Parameters:
  135. /// - queue: `DispatchQueue` on which to call the `listener` closure. `.main` by default.
  136. /// - listener: `Listener` closure called when reachability changes.
  137. ///
  138. /// - Returns: `true` if listening was started successfully, `false` otherwise.
  139. @preconcurrency
  140. @discardableResult
  141. open func startListening(onQueue queue: DispatchQueue = .main,
  142. onUpdatePerforming listener: @escaping Listener) -> Bool {
  143. stopListening()
  144. mutableState.write { state in
  145. state.listenerQueue = queue
  146. state.listener = listener
  147. }
  148. let weakManager = WeakManager(manager: self)
  149. var context = SCNetworkReachabilityContext(
  150. version: 0,
  151. info: Unmanaged.passUnretained(weakManager).toOpaque(),
  152. retain: { info in
  153. let unmanaged = Unmanaged<WeakManager>.fromOpaque(info)
  154. _ = unmanaged.retain()
  155. return UnsafeRawPointer(unmanaged.toOpaque())
  156. },
  157. release: { info in
  158. let unmanaged = Unmanaged<WeakManager>.fromOpaque(info)
  159. unmanaged.release()
  160. },
  161. copyDescription: { info in
  162. let unmanaged = Unmanaged<WeakManager>.fromOpaque(info)
  163. let weakManager = unmanaged.takeUnretainedValue()
  164. let description = weakManager.manager?.flags?.readableDescription ?? "nil"
  165. return Unmanaged.passRetained(description as CFString)
  166. }
  167. )
  168. let callback: SCNetworkReachabilityCallBack = { _, flags, info in
  169. guard let info else { return }
  170. let weakManager = Unmanaged<WeakManager>.fromOpaque(info).takeUnretainedValue()
  171. weakManager.manager?.notifyListener(flags)
  172. }
  173. let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue)
  174. let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context)
  175. // Manually call listener to give initial state, since the framework may not.
  176. if let currentFlags = flags {
  177. reachabilityQueue.async {
  178. self.notifyListener(currentFlags)
  179. }
  180. }
  181. return callbackAdded && queueAdded
  182. }
  183. /// Stops listening for changes in network reachability status.
  184. open func stopListening() {
  185. SCNetworkReachabilitySetCallback(reachability, nil, nil)
  186. SCNetworkReachabilitySetDispatchQueue(reachability, nil)
  187. mutableState.write { state in
  188. state.listener = nil
  189. state.listenerQueue = nil
  190. state.previousStatus = nil
  191. }
  192. }
  193. // MARK: - Internal - Listener Notification
  194. /// Calls the `listener` closure of the `listenerQueue` if the computed status hasn't changed.
  195. ///
  196. /// - Note: Should only be called from the `reachabilityQueue`.
  197. ///
  198. /// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status.
  199. func notifyListener(_ flags: SCNetworkReachabilityFlags) {
  200. let newStatus = NetworkReachabilityStatus(flags)
  201. mutableState.write { [newStatus] state in
  202. guard state.previousStatus != newStatus else { return }
  203. state.previousStatus = newStatus
  204. let listener = state.listener
  205. state.listenerQueue?.async { listener?(newStatus) }
  206. }
  207. }
  208. private final class WeakManager {
  209. weak var manager: NetworkReachabilityManager?
  210. init(manager: NetworkReachabilityManager?) {
  211. self.manager = manager
  212. }
  213. }
  214. }
  215. // MARK: -
  216. extension SCNetworkReachabilityFlags {
  217. var isReachable: Bool { contains(.reachable) }
  218. var isConnectionRequired: Bool { contains(.connectionRequired) }
  219. var canConnectAutomatically: Bool { contains(.connectionOnDemand) || contains(.connectionOnTraffic) }
  220. var canConnectWithoutUserInteraction: Bool { canConnectAutomatically && !contains(.interventionRequired) }
  221. var isActuallyReachable: Bool { isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) }
  222. var isCellular: Bool {
  223. #if os(iOS) || os(tvOS) || os(visionOS)
  224. return contains(.isWWAN)
  225. #else
  226. return false
  227. #endif
  228. }
  229. /// Human readable `String` for all states, to help with debugging.
  230. var readableDescription: String {
  231. let W = isCellular ? "W" : "-"
  232. let R = isReachable ? "R" : "-"
  233. let c = isConnectionRequired ? "c" : "-"
  234. let t = contains(.transientConnection) ? "t" : "-"
  235. let i = contains(.interventionRequired) ? "i" : "-"
  236. let C = contains(.connectionOnTraffic) ? "C" : "-"
  237. let D = contains(.connectionOnDemand) ? "D" : "-"
  238. let l = contains(.isLocalAddress) ? "l" : "-"
  239. let d = contains(.isDirect) ? "d" : "-"
  240. let a = contains(.connectionAutomatic) ? "a" : "-"
  241. return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)\(a)"
  242. }
  243. }
  244. #endif