// Copyright (c) 2014, Ashley Mills import SystemConfiguration import Foundation public enum ReachabilityError: Error { case FailedToCreateWithAddress(sockaddr_in) case FailedToCreateWithHostname(String) case UnableToSetCallback case UnableToSetDispatchQueue case UnableToGetInitialFlags } @available(*, unavailable, renamed: "Notification.Name.availabilityChanged") public let AvailabilityChangedNotification = NSNotification.Name("AvailabilityChangedNotification") public extension Notification.Name { static let availabilityChanged = Notification.Name("availabilityChanged") } public class Availability { public typealias NetworkReachable = (Availability) -> () public typealias NetworkUnreachable = (Availability) -> () @available(*, unavailable, renamed: "Connection") public enum NetworkStatus: CustomStringConvertible { case notReachable, reachableViaWiFi, reachableViaWWAN public var description: String { switch self { case .reachableViaWWAN: return "Cellular" case .reachableViaWiFi: return "WiFi" case .notReachable: return "No Connection" } } } public enum Connection: CustomStringConvertible { case none, wifi, cellular public var description: String { switch self { case .cellular: return "Cellular" case .wifi: return "WiFi" case .none: return "No Connection" } } } public var whenReachable: NetworkReachable? public var whenUnreachable: NetworkUnreachable? @available(*, deprecated, renamed: "allowsCellularConnection") public let reachableOnWWAN: Bool = true /// Set to `false` to force Reachability.connection to .none when on cellular connection (default value `true`) public var allowsCellularConnection: Bool // The notification center on which "reachability changed" events are being posted public var notificationCenter: NotificationCenter = NotificationCenter.default @available(*, deprecated, renamed: "connection.description") public var currentReachabilityString: String { return "\(connection)" } @available(*, unavailable, renamed: "connection") public var currentReachabilityStatus: Connection { return connection } public var connection: Connection { if flags == nil { try? setReachabilityFlags() } switch flags?.connection { case .none?, nil: return .none case .cellular?: return allowsCellularConnection ? .cellular : .none case .wifi?: return .wifi } } fileprivate var isRunningOnDevice: Bool = { #if targetEnvironment(simulator) return false #else return true #endif }() fileprivate var notifierRunning = false fileprivate let ref: SCNetworkReachability fileprivate let serialQueue: DispatchQueue fileprivate(set) var flags: SCNetworkReachabilityFlags? { didSet { guard flags != oldValue else { return } availabilityChanged() } } required public init(availabilityRef: SCNetworkReachability, queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { self.allowsCellularConnection = true self.ref = availabilityRef self.serialQueue = DispatchQueue(label: "uk.co.ashleymills.availability", qos: queueQoS, target: targetQueue) } public convenience init?(hostname: String, queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else { return nil } self.init(availabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue) } public convenience init?(queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { var zeroAddress = sockaddr() zeroAddress.sa_len = UInt8(MemoryLayout.size) zeroAddress.sa_family = sa_family_t(AF_INET) guard let ref = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else { return nil } self.init(availabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue) } deinit { stopNotifier() } } public extension Availability { // MARK: - *** Notifier methods *** func startNotifier() throws { guard !notifierRunning else { return } let callback: SCNetworkReachabilityCallBack = { (reachability, flags, info) in guard let info = info else { return } let reachability = Unmanaged.fromOpaque(info).takeUnretainedValue() reachability.flags = flags } var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) context.info = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) if !SCNetworkReachabilitySetCallback(ref, callback, &context) { stopNotifier() throw ReachabilityError.UnableToSetCallback } if !SCNetworkReachabilitySetDispatchQueue(ref, serialQueue) { stopNotifier() throw ReachabilityError.UnableToSetDispatchQueue } // Perform an initial check try setReachabilityFlags() notifierRunning = true } func stopNotifier() { defer { notifierRunning = false } SCNetworkReachabilitySetCallback(ref, nil, nil) SCNetworkReachabilitySetDispatchQueue(ref, nil) } // MARK: - *** Connection test methods *** @available(*, deprecated, message: "Please use `connection != .none`") var isReachable: Bool { return connection != .none } @available(*, deprecated, message: "Please use `connection == .cellular`") var isReachableViaWWAN: Bool { // Check we're not on the simulator, we're REACHABLE and check we're on WWAN return connection == .cellular } @available(*, deprecated, message: "Please use `connection == .wifi`") var isReachableViaWiFi: Bool { return connection == .wifi } var description: String { guard let flags = flags else { return "unavailable flags" } let W = isRunningOnDevice ? (flags.isOnWWANFlagSet ? "W" : "-") : "X" let R = flags.isReachableFlagSet ? "R" : "-" let c = flags.isConnectionRequiredFlagSet ? "c" : "-" let t = flags.isTransientConnectionFlagSet ? "t" : "-" let i = flags.isInterventionRequiredFlagSet ? "i" : "-" let C = flags.isConnectionOnTrafficFlagSet ? "C" : "-" let D = flags.isConnectionOnDemandFlagSet ? "D" : "-" let l = flags.isLocalAddressFlagSet ? "l" : "-" let d = flags.isDirectFlagSet ? "d" : "-" return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)" } } fileprivate extension Availability { func setReachabilityFlags() throws { try serialQueue.sync { [unowned self] in var flags = SCNetworkReachabilityFlags() if !SCNetworkReachabilityGetFlags(self.ref, &flags) { self.stopNotifier() throw ReachabilityError.UnableToGetInitialFlags } self.flags = flags } } func availabilityChanged() { let block = connection != .none ? whenReachable : whenUnreachable DispatchQueue.main.async { [weak self] in guard let self = self else { return } block?(self) self.notificationCenter.post(name: .availabilityChanged, object: self) } } } extension SCNetworkReachabilityFlags { typealias Connection = Availability.Connection var connection: Connection { guard isReachableFlagSet else { return .none } // If we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi #if targetEnvironment(simulator) return .wifi #else var connection = Connection.none if !isConnectionRequiredFlagSet { connection = .wifi } if isConnectionOnTrafficOrDemandFlagSet { if !isInterventionRequiredFlagSet { connection = .wifi } } if isOnWWANFlagSet { connection = .cellular } return connection #endif } var isOnWWANFlagSet: Bool { #if os(iOS) return contains(.isWWAN) #else return false #endif } var isReachableFlagSet: Bool { return contains(.reachable) } var isConnectionRequiredFlagSet: Bool { return contains(.connectionRequired) } var isInterventionRequiredFlagSet: Bool { return contains(.interventionRequired) } var isConnectionOnTrafficFlagSet: Bool { return contains(.connectionOnTraffic) } var isConnectionOnDemandFlagSet: Bool { return contains(.connectionOnDemand) } var isConnectionOnTrafficOrDemandFlagSet: Bool { return !intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty } var isTransientConnectionFlagSet: Bool { return contains(.transientConnection) } var isLocalAddressFlagSet: Bool { return contains(.isLocalAddress) } var isDirectFlagSet: Bool { return contains(.isDirect) } var isConnectionRequiredAndTransientFlagSet: Bool { return intersection([.connectionRequired, .transientConnection]) == [.connectionRequired, .transientConnection] } }