Skip to content

Commit

Permalink
Refactor Reachability (Alamofire#2915)
Browse files Browse the repository at this point in the history
* Refactor reachability implementation.

* Additional cleanup and tests.

* Make listener queue private and pass it when listening.

* Protect previous status.
  • Loading branch information
jshier committed Aug 25, 2019
1 parent a13c697 commit 052c2c2
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 162 deletions.
8 changes: 2 additions & 6 deletions Example/Source/MasterViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,16 @@ class MasterViewController: UITableViewController {

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.section == 3 && indexPath.row == 0 {
print("Reachability Status: \(reachability.networkReachabilityStatus)")
print("Reachability Status: \(reachability.status)")
tableView.deselectRow(at: indexPath, animated: true)
}
}

// MARK: - Private - Reachability

private func monitorReachability() {
reachability = NetworkReachabilityManager(host: "www.apple.com")

reachability.listener = { status in
NetworkReachabilityManager.default?.startListening { status in
print("Reachability Status Changed: \(status)")
}

reachability.startListening()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
Expand Down
242 changes: 138 additions & 104 deletions Source/NetworkReachabilityManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
// THE SOFTWARE.
//

#if !os(watchOS)
#if !(os(watchOS) || os(Linux))

import Foundation
import SystemConfiguration

/// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both WWAN and
/// The `NetworkReachabilityManager` class listens for reachability changes of hosts and addresses for both cellular and
/// WiFi network interfaces.
///
/// Reachability can be used to determine background information about why a network operation failed, or to retry
Expand All @@ -42,65 +42,86 @@ open class NetworkReachabilityManager {
case notReachable
/// The network is reachable on the associated `ConnectionType`.
case reachable(ConnectionType)
}

/// Defines the various connection types detected by reachability flags.
public enum ConnectionType {
/// The connection type is either over Ethernet or WiFi.
case ethernetOrWiFi
/// The connection type is a WWAN connection.
case wwan
init(_ flags: SCNetworkReachabilityFlags) {
guard flags.isActuallyReachable else { self = .notReachable; return }

var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)

if flags.isCellular { networkStatus = .reachable(.cellular) }

self = networkStatus
}

/// Defines the various connection types detected by reachability flags.
public enum ConnectionType {
/// The connection type is either over Ethernet or WiFi.
case ethernetOrWiFi
/// The connection type is a cellular connection.
case cellular
}
}

/// A closure executed when the network reachability status changes. The closure takes a single argument: the
/// network reachability status.
public typealias Listener = (NetworkReachabilityStatus) -> Void

/// Default `NetworkReachabilityManager` for the zero address and a `listenerQueue` of `.main`.
public static let `default` = NetworkReachabilityManager()

// MARK: - Properties

/// Whether the network is currently reachable.
open var isReachable: Bool { return isReachableOnWWAN || isReachableOnEthernetOrWiFi }
open var isReachable: Bool { return isReachableOnCellular || isReachableOnEthernetOrWiFi }

/// Whether the network is currently reachable over the WWAN interface.
open var isReachableOnWWAN: Bool { return networkReachabilityStatus == .reachable(.wwan) }
/// Whether the network is currently reachable over the cellular interface.
///
/// - Note: Using this property to decide whether to make a high or low bandwidth request is not recommended.
/// Instead, set the `allowsCellularAccess` on any `URLRequest`s being issued.
///
open var isReachableOnCellular: Bool { return status == .reachable(.cellular) }

/// Whether the network is currently reachable over Ethernet or WiFi interface.
open var isReachableOnEthernetOrWiFi: Bool { return networkReachabilityStatus == .reachable(.ethernetOrWiFi) }

/// The current network reachability status.
open var networkReachabilityStatus: NetworkReachabilityStatus {
guard let flags = self.flags else { return .unknown }
return networkReachabilityStatusForFlags(flags)
}
open var isReachableOnEthernetOrWiFi: Bool { return status == .reachable(.ethernetOrWiFi) }

/// The dispatch queue to execute the `listener` closure on.
open var listenerQueue: DispatchQueue = DispatchQueue.main

/// A closure executed when the network reachability status changes.
open var listener: Listener?
/// `DispatchQueue` on which reachability will update.
public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue")

/// Flags of the current reachability type, if any.
open var flags: SCNetworkReachabilityFlags? {
var flags = SCNetworkReachabilityFlags()

if SCNetworkReachabilityGetFlags(reachability, &flags) {
return flags
}
return (SCNetworkReachabilityGetFlags(reachability, &flags)) ? flags : nil
}

return nil
/// The current network reachability status.
open var status: NetworkReachabilityStatus {
return flags.map(NetworkReachabilityStatus.init) ?? .unknown
}

/// A closure executed when the network reachability status changes.
private var listener: Listener?

/// `DispatchQueue` on which listeners will be called.
private var listenerQueue: DispatchQueue?

/// `SCNetworkReachability` instance providing notifications.
private let reachability: SCNetworkReachability
/// Reachability flags of the previous reachability state.
open var previousFlags: SCNetworkReachabilityFlags

/// Protected storage for the previous status.
private let previousStatus = Protector<NetworkReachabilityStatus?>(nil)

// MARK: - Initialization

/// Creates an instance with the specified host.
///
/// - Parameter host: Host used to evaluate network reachability. Must not include the scheme (i.e. `https`).
/// - Note: The `host` value must *not* contain a scheme, just the hostname.
///
/// - Parameters:
/// - host: Host used to evaluate network reachability. Must *not* include the scheme (e.g. `https`).
public convenience init?(host: String) {
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }

self.init(reachability: reachability)
}

Expand All @@ -109,24 +130,17 @@ open class NetworkReachabilityManager {
/// Reachability treats the 0.0.0.0 address as a special token that causes it to monitor the general routing
/// status of the device, both IPv4 and IPv6.
public convenience init?() {
var address = sockaddr_in()
address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
address.sin_family = sa_family_t(AF_INET)
var zero = sockaddr()
zero.sa_len = UInt8(MemoryLayout<sockaddr>.size)
zero.sa_family = sa_family_t(AF_INET)

guard let reachability = withUnsafePointer(to: &address, { pointer in
return pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size) {
return SCNetworkReachabilityCreateWithAddress(nil, $0)
}
}) else { return nil }
guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil }

self.init(reachability: reachability)
}

private init(reachability: SCNetworkReachability) {
self.reachability = reachability

// Set the previous flags to an unreserved value to represent unknown status
self.previousFlags = SCNetworkReachabilityFlags(rawValue: 1 << 30)
}

deinit {
Expand All @@ -137,87 +151,107 @@ open class NetworkReachabilityManager {

/// Starts listening for changes in network reachability status.
///
/// - Note: Stops and removes any existing listener.
///
/// - Parameters:
/// - queue: `DispatchQueue` on which to call the `listener` closure. `.main` by default.
/// - listener: `Listener` closure called when reachability changes.
///
/// - Returns: `true` if listening was started successfully, `false` otherwise.
@discardableResult
open func startListening() -> Bool {
var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
context.info = Unmanaged.passUnretained(self).toOpaque()

let callbackEnabled = SCNetworkReachabilitySetCallback(
reachability,
{ (_, flags, info) in
let reachability = Unmanaged<NetworkReachabilityManager>.fromOpaque(info!).takeUnretainedValue()
reachability.notifyListener(flags)
},
&context
)

let queueEnabled = SCNetworkReachabilitySetDispatchQueue(reachability, listenerQueue)

listenerQueue.async {
guard let flags = self.flags else { return }
self.notifyListener(flags)
open func startListening(onQueue queue: DispatchQueue = .main,
onUpdatePerforming listener: @escaping Listener) -> Bool {
stopListening()

listenerQueue = queue
self.listener = listener

var context = SCNetworkReachabilityContext(version: 0,
info: Unmanaged.passRetained(self).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil)
let callback: SCNetworkReachabilityCallBack = { (target, flags, info) in
guard let info = info else { return }

let instance = Unmanaged<NetworkReachabilityManager>.fromOpaque(info).takeUnretainedValue()
instance.notifyListener(flags)
}

return callbackEnabled && queueEnabled
let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue)
let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context)

// Manually call listener to give initial state, since the framework may not.
if let currentFlags = flags {
reachabilityQueue.async {
self.notifyListener(currentFlags)
}
}

return callbackAdded && queueAdded
}

/// Stops listening for changes in network reachability status.
open func stopListening() {
SCNetworkReachabilitySetCallback(reachability, nil, nil)
SCNetworkReachabilitySetDispatchQueue(reachability, nil)
previousStatus.write { $0 = nil }
listenerQueue = nil
listener = nil
}

// MARK: - Internal - Listener Notification


/// Calls the `listener` closure of the `listenerQueue` if the computed status hasn't changed.
///
/// - Note: Should only be called from the `reachabilityQueue`.
///
/// - Parameter flags: `SCNetworkReachabilityFlags` to use to calculate the status.
func notifyListener(_ flags: SCNetworkReachabilityFlags) {
guard previousFlags != flags else { return }
previousFlags = flags

listener?(networkReachabilityStatusForFlags(flags))
}

// MARK: - Internal - Network Reachability Status

func networkReachabilityStatusForFlags(_ flags: SCNetworkReachabilityFlags) -> NetworkReachabilityStatus {
guard isNetworkReachable(with: flags) else { return .notReachable }

var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)

#if os(iOS)
if flags.contains(.isWWAN) { networkStatus = .reachable(.wwan) }
#endif

return networkStatus
}

func isNetworkReachable(with flags: SCNetworkReachabilityFlags) -> Bool {
let isReachable = flags.contains(.reachable)
let needsConnection = flags.contains(.connectionRequired)
let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic)
let canConnectWithoutUserInteraction = canConnectAutomatically && !flags.contains(.interventionRequired)

return isReachable && (!needsConnection || canConnectWithoutUserInteraction)
let newStatus = NetworkReachabilityStatus(flags)

previousStatus.write { previousStatus in
guard previousStatus != newStatus else { return }

previousStatus = newStatus

listenerQueue?.async { self.listener?(newStatus) }
}
}
}

// MARK: -

extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable {
public static func ==(
lhs: NetworkReachabilityManager.NetworkReachabilityStatus,
rhs: NetworkReachabilityManager.NetworkReachabilityStatus)
-> Bool
{
switch (lhs, rhs) {
case (.unknown, .unknown), (.notReachable, .notReachable):
return true
case let (.reachable(lhsConnectionType), .reachable(rhsConnectionType)):
return lhsConnectionType == rhsConnectionType
default:
return false
}
extension NetworkReachabilityManager.NetworkReachabilityStatus: Equatable { }

extension SCNetworkReachabilityFlags {
var isReachable: Bool { return contains(.reachable) }
var isConnectionRequired: Bool { return contains(.connectionRequired) }
var canConnectAutomatically: Bool { return contains(.connectionOnDemand) || contains(.connectionOnTraffic) }
var canConnectWithoutUserInteraction: Bool { return canConnectAutomatically && !contains(.interventionRequired) }
var isActuallyReachable: Bool { return isReachable && (!isConnectionRequired || canConnectWithoutUserInteraction) }
var isCellular: Bool {
#if os(iOS) || os(tvOS)
return contains(.isWWAN)
#else
return false
#endif
}
}

/// Human readable `String` for all states, to help with debugging.
var readableDescription: String {
let W = isCellular ? "W" : "-"
let R = isReachable ? "R" : "-"
let c = isConnectionRequired ? "c" : "-"
let t = contains(.transientConnection) ? "t" : "-"
let i = contains(.interventionRequired) ? "i" : "-"
let C = contains(.connectionOnTraffic) ? "C" : "-"
let D = contains(.connectionOnDemand) ? "D" : "-"
let l = contains(.isLocalAddress) ? "l" : "-"
let d = contains(.isDirect) ? "d" : "-"
let a = contains(.connectionAutomatic) ? "a" : "-"

return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)\(a)"
}
}
#endif
2 changes: 1 addition & 1 deletion Source/RequestInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public protocol RequestRetrier {
/// - request: `Request` that failed due to the provided `Error`.
/// - session: `Session` that produced the `Request`.
/// - error: `Error` encountered while executing the `Request`.
/// - completion: Completion closure to be executed when a retry decision has been deterined.
/// - completion: Completion closure to be executed when a retry decision has been determined.
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void)
}

Expand Down
Loading

0 comments on commit 052c2c2

Please sign in to comment.