Now, networking is one of the core components of almost any mobile app. Most applications store, retrieve, analyze, and provide data to u using a network connection. Without networking, most apps can’t exist at all.

Different situations may occur and apps may be used in a different location with or without an internet connection. Sometimes we would like to notify the user that some connection interruptions have been detected and so the work of the app may be unstable. This indeed improves UX.

I don’t want to cover good practices related to network connectivity usage - what we should do and what shouldn’t, instead, I just want to cover the part that allows performing check the connection itself.

In case if u have some doubts about the network connectivity checking process, I can only recommend review networking documentation available here and review Apple WWDC video (#712, #713)

I should also mention that according to doc:

” Checking the reachability flag does not guarantee that your traffic will never be sent over a cellular connection.”

So my advice before we start - make sure that the purpose of the network connection checking process does not reduce the functionality of your app, and u use it just as an informative mechanism, in other cases u can lie to your user and provide poor UX.

The most popular ways to check internet connection are created thanks to next frameworks:

  • SystemConfiguration
  • Network

NetworkReachabilityProvider

Before we dive into details of each framework usage, we may want to configure some requirements for our NetworkStatus monitor. I have created NetworkReachabilityProvider protocol, that describes a possible way of determining information about network connection change:

import Foundation
import Combine

protocol NetworkReachabilityProvider: class {
    var networkStatusHandler: AnyPublisher<NetworkReachabilityStatus, Never> { get }
    
    func stopListening()
    func startListening() -> AnyPublisher<Bool, Never>
}

public enum NetworkReachabilityStatus: Equatable {
    public enum ConnectionType {
        case ethernetOrWiFi
        case wwan
    }
    
    case unknown
    case notReachable
    case reachable(ConnectionType)
}

As u can see, this protocol requires that we start and stop monitoring and does not provide u current state. Instead networkStatusHandler will return u current state as soon as u start listening.

U may found such a way not useful for some cases - for example when u need to know instantly the status of a network connection. But I do this intentionally because u shouldn’t have such a situation at all - remember that I mention above “make sure that purpose of network connection checking process does not reduce the functionality of your app”. So u don’t need to know the instant status at all. And for informative purposes, u can use listeners and appropriate publishers.

A good point to mention - there are a lot of discussions about reachability (here and here for example) and it’s non 100% precise result. And this is one more reason to not use this option for controlling how u logic works. Instead, better use the post-factum result of network requests and adjust logic using actual and REAL results.

SystemConfiguration

This framework provides for us various mechanisms that allow applications to access a device’s network configuration settings.

The System Configuration framework provides powerful, flexible support for establishing and maintaining access to a configurable network and system resources. It offers your application the ability to determine, set, and maintain configuration settings and to detect and respond dynamically to changes in that information. more

reference to api

The most interesting part for us - is the chapter dedicated to reachability - Determining Reachability and Getting Connected. According to it we should perform a few main steps to get things done:

  • Create a reference for your target remote host you can use in other reachability functions.
  • Add the target to your run loop.
  • Provide a callback function that’s called when the reachability status of your target changes.
  • Determine if the target is reachable.

Implementation

The API allows us to use network host or node name SCNetworkReachabilityCreateWithName we want to check or using the specified network address struct sockaddr_in - SCNetworkReachabilityCreateWithAddress. Third option - SCNetworkReachabilityCreateWithAddressPair - allow to create a reachability reference to the specified network address.

Checking host sometimes is useful, but this means that logic will depend on the availability of some host.

I prefer to use SCNetworkReachabilityCreateWithAddress - this does not require any host specification or specific network address.

// Socket address, internet style
var initialAddress = sockaddr_in()
// The length member, sin_len, was added with 4.3BSD-Reno,
// when support for the OSI protocols was added
initialAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
// internetwork: UDP, TCP, etc
initialAddress.sin_family = sa_family_t(AF_INET)
    
if let initialReachability: SCNetworkReachability =
    withUnsafePointer(
        to: &initialAddress, {
            $0.withMemoryRebound(
                to: sockaddr.self,
                capacity: MemoryLayout<sockaddr>.size
            ) {
                SCNetworkReachabilityCreateWithAddress(nil, $0)
            }
        }) {
   // do next steps...
} else {
   // handle failure...
}

Using SCNetworkReachability we can extract SCNetworkReachabilityFlags and retrive current state of network connectivity status:

var flags = SCNetworkReachabilityFlags()
if SCNetworkReachabilityGetFlags(reachability, &flags) {
    return flags
}

Function to retrive status may be done as next:

private func reachabilityOfNetworkFor(
    _ flags: SCNetworkReachabilityFlags
) -> NetworkReachabilityStatus {
    var networkStatus: NetworkReachabilityStatus = .unknown
    
    let isReachable = flags.contains(.reachable)
    let needsConnection = flags.contains(.connectionRequired)
    
    let canConnectAutomatically = flags.contains(.connectionOnDemand) ||
        flags.contains(.connectionOnTraffic)
    let canConnectWithoutUserInteraction = canConnectAutomatically &&
        !flags.contains(.interventionRequired)
    let isNetworkAvailableFlag = isReachable &&
        (!needsConnection || canConnectWithoutUserInteraction)
    
    if isNetworkAvailableFlag {
        networkStatus = .reachable(.ethernetOrWiFi)
        
        #if os(iOS)
        if flags.contains(.isWWAN) {
            networkStatus = .reachable(.wwan)
        }
        #endif
        
    } else {
        networkStatus = .notReachable
    }
    
    return networkStatus
}

This is all good, but we also would like to be informed when something changed. To configure such behavior we may use SCNetworkReachabilitySetCallback.

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
        if let info = info {
            let reachability = Unmanaged<NetworkReachability>
                .fromOpaque(info)
                .takeUnretainedValue()
            reachability.notifyListener(flags)
        }
    },
    &context
)
    
let queueEnabled = SCNetworkReachabilitySetDispatchQueue(
    reachability,
    handlerQueue
)
    
handlerQueue.async { [weak self] in
    self?.notifyListener(self?.flagsForCurrentReachability ?? .init())
}

Alternative way is to use SCNetworkReachabilityScheduleWithRunLoop instead of SCNetworkReachabilitySetDispatchQueue. I prefere use DispatchQueue, bacause configuration is much easier.

notifyListener is a function that simply analyzes new flags and posts updates.

And we also need to have an option to stop the process:

SCNetworkReachabilitySetCallback(reachability, nil, nil)
SCNetworkReachabilitySetDispatchQueue(reachability, nil)

Putting all together we can get next:

import Foundation
import SystemConfiguration
import Combine

final class NetworkReachability: NetworkReachabilityProvider {
    
    public var networkStatusHandler: AnyPublisher<NetworkReachabilityStatus, Never> {
        reachabilityNotifier.eraseToAnyPublisher()
    }
    
    private let handlerQueue: DispatchQueue = .main
    private let reachability: SCNetworkReachability
    private var previousFlags: SCNetworkReachabilityFlags
    private let reachabilityNotifier: PassthroughSubject<NetworkReachabilityStatus, Never>
    
    private var flagsForCurrentReachability: SCNetworkReachabilityFlags? {
        var flags = SCNetworkReachabilityFlags()
        if SCNetworkReachabilityGetFlags(reachability, &flags) {
            return flags
        }
        
        return nil
    }
    
    // MARK: - Lifecycle
    
    convenience init?() {
        // Socket address, internet style
        var initialAddress = sockaddr_in()
        // The length member, sin_len, was added with 4.3BSD-Reno,
        // when support for the OSI protocols was added
        initialAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
        // internetwork: UDP, TCP, etc
        initialAddress.sin_family = sa_family_t(AF_INET)
        
        if let initialReachability: SCNetworkReachability =
            withUnsafePointer(
                to: &initialAddress, {
                    $0.withMemoryRebound(
                        to: sockaddr.self,
                        capacity: MemoryLayout<sockaddr>.size
                    ) {
                        SCNetworkReachabilityCreateWithAddress(nil, $0)
                    }
                }) {
            self.init(reachability: initialReachability)
        } else {
            return nil
        }
    }
    
    private init(reachability: SCNetworkReachability) {
        self.reachability = reachability
        self.previousFlags = .init()
        self.reachabilityNotifier = PassthroughSubject<NetworkReachabilityStatus, Never>()
    }
    
    deinit {
        defer {
            reachabilityNotifier.send(completion: .finished)
        }
        stopListening()
    }
    
    // MARK: - Public
    
    func stopListening() {
        SCNetworkReachabilitySetCallback(reachability, nil, nil)
        SCNetworkReachabilitySetDispatchQueue(reachability, nil)
    }
    
    @discardableResult
    func startListening() -> AnyPublisher<Bool, Never> {
        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
                if let info = info {
                    let reachability = Unmanaged<NetworkReachability>
                        .fromOpaque(info)
                        .takeUnretainedValue()
                    reachability.notifyListener(flags)
                }
            },
            &context
        )
        
        let queueEnabled = SCNetworkReachabilitySetDispatchQueue(
            reachability,
            handlerQueue
        )
        
        handlerQueue.async { [weak self] in
            self?.notifyListener(self?.flagsForCurrentReachability ?? .init())
        }
        
        return Just(callbackEnabled && queueEnabled).eraseToAnyPublisher()
    }
    
    // MARK: - Private
    
    private func reachabilityOfNetworkFor(
        _ flags: SCNetworkReachabilityFlags
    ) -> NetworkReachabilityStatus {
        var networkStatus: NetworkReachabilityStatus = .unknown
        
        let isReachable = flags.contains(.reachable)
        let needsConnection = flags.contains(.connectionRequired)
        
        let canConnectAutomatically = flags.contains(.connectionOnDemand) ||
            flags.contains(.connectionOnTraffic)
        let canConnectWithoutUserInteraction = canConnectAutomatically &&
            !flags.contains(.interventionRequired)
        let isNetworkAvailableFlag = isReachable &&
            (!needsConnection || canConnectWithoutUserInteraction)
        
        if isNetworkAvailableFlag {
            networkStatus = .reachable(.ethernetOrWiFi)
            
            #if os(iOS)
            if flags.contains(.isWWAN) {
                networkStatus = .reachable(.wwan)
            }
            #endif
            
        } else {
            networkStatus = .notReachable
        }
        
        return networkStatus
    }
    
    private func notifyListener(_ flags: SCNetworkReachabilityFlags) {
        guard previousFlags != flags else {
            return
        }
        previousFlags = flags
        let currentNetworkStatus = reachabilityOfNetworkFor(flags)
        
        reachabilityNotifier.send(currentNetworkStatus)
    }
}

3rd party solutions

If u don’t want to spend some time and check how to implement this u may use already existing great solutions from Apple or some other developers such as Ashley Mills

To adopt Reachability from Ashley Mills we may wrap her code in to next:

import Combine

final class ReachabilityAshley: NetworkReachabilityProvider {
    public var networkStatusHandler: AnyPublisher<NetworkReachabilityStatus, Never> {
        reachabilityNotifier.eraseToAnyPublisher()
    }
    
    private var reachability: Reachability
    private let reachabilityNotifier: PassthroughSubject<NetworkReachabilityStatus, Never>
    private var token: AnyCancellable?
    
    init?() {
        do {
            self.reachability = try Reachability()
            self.reachabilityNotifier = PassthroughSubject<NetworkReachabilityStatus, Never>()
        } catch {
            return nil
        }
    }
    
    func stopListening() {
        token?.cancel()
        token = nil
        reachability.stopNotifier()
    }
    
    @discardableResult
    func startListening() -> AnyPublisher<Bool, Never> {
        do {
            token = NotificationCenter.default
                .publisher(for: Notification.Name.reachabilityChanged)
                .sink(receiveValue: checkForReachability)
            try reachability.startNotifier()
            return Just(true).eraseToAnyPublisher()
        } catch {
            token?.cancel()
            return Just(false).eraseToAnyPublisher()
        }
    }
    
    deinit {
        stopListening()
    }
    
    // MARK: - Private
    
    private func checkForReachability(_ notification: Notification) {
        let networkReachability = notification.object as? Reachability
        var networkStatus: NetworkReachabilityStatus = .unknown

        if let remoteHostStatus = networkReachability?.connection {
            switch remoteHostStatus {
            case .wifi,
                 .cellular:
                networkStatus = .reachable(.ethernetOrWiFi)
            case .unavailable:
                networkStatus = .notReachable
            }
        }
        
        reachabilityNotifier.send(networkStatus)
    }
}

Network

Network is a new framework available for iOS 12+ and dedicated for creating network connections to send and receive data using transport and security protocols.

Within its rich functionality Network has NWPathMonitor class that can be used for* monitor and react to network changes*. Usage is much simpler in comparison to SystemConfiguration. To make it works, we simply create an object and subscribe to any changes, analyzing response:

import Foundation
import Combine
import Network

final class NetworkMonitor: NetworkReachabilityProvider {
    public var networkStatusHandler: AnyPublisher<NetworkReachabilityStatus, Never> {
        reachabilityNotifier.eraseToAnyPublisher()
    }
    
    private let monitor: NWPathMonitor = .init()
    private let handlerQueue: DispatchQueue = .main
    private let reachabilityNotifier: PassthroughSubject<NetworkReachabilityStatus, Never>
    
    // MARK: - Lifecycle
    
    public init() {
        reachabilityNotifier = PassthroughSubject<NetworkReachabilityStatus, Never>()
        configureListener()
    }
    
    deinit {
        monitor.cancel()
    }
    
    // MARK: - Public
    
    public func stopListening() {
        monitor.cancel()
    }
    
    @discardableResult
    public func startListening() -> AnyPublisher<Bool, Never> {
        monitor.start(queue: handlerQueue)
        return Just(true).eraseToAnyPublisher()
    }
    
    // MARK: - Private
    
    private func configureListener() {
        var networkStatus: NetworkReachabilityStatus = .unknown

        monitor.pathUpdateHandler = { [weak self] path in
            switch path.status {
            case .satisfied:
                if path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet) {
                    networkStatus = .reachable(.ethernetOrWiFi)
                } else {
                    networkStatus = .reachable(.wwan)
                }
            case .unsatisfied,
                 .requiresConnection:
                networkStatus = .notReachable
            @unknown default:
                networkStatus = .notReachable
            }
            
            self?.reachabilityNotifier.send(networkStatus)
        }
    }
}

As u can see - such solutions are much simpler and compact. But under the hood, it’s using the same mechanisms.

To demonstrate usage of all variants, I created demo app:

demo

Resources

download source code