/ IOS, SWIFT

Working with an Internet Connection on iOS with Swift: Best Practices

Networking is an integral part of most iOS applications. A common network-related task is Internet connectivity detection. Typically, it appears in three scenarios:

  • Checking connectivity before firing an HTTP request.
  • Disabling or enabling app features based on network connectivity status.
  • Attaching constraints to network operations, e.g., disabling large file download via cellular.

The most popular answers on how to detect network connectivity status on iOS suggest using SCNetworkReachability. In this article, let’s discuss why this solution is less than optimal, and lay out best practices of working with the Internet connection recommended by Apple.

Checking Connectivity Before Firing a Request

In its documentation, Apple says that we should not check Internet connection before firing an HTTP request. From Networking Overview:

The SCNetworkReachability API is not intended for use as a preflight mechanism for determining network connectivity. […]

We can hear similar pieces of advice in multiple WWDC sessions. From Advances in Networking, Part 1, at 50:00:

Pre-flight check is a very bad indicator of where your flow will end up on.

From Advances in Networking, Part 2, at 56:00

Pre-flight checks have inherited race conditions.

From Networking Best Practices, at 33:00:

Don’t check the Reachability object to determine whether something is available. It can be deceptive in some cases.

The reasons for these statements are next:

  • Knowing in advance how Wi-Fi will perform until you try is impossible [Advances in Networking, Part 2, 57:00].
  • Internet connection pre-flight checks have inherent race conditions, give false positives and false negatives, time-of-check to time-of-use problem [Apple dev forum thread].
  • Reachability will report unreachable if the networking hardware is powered down. Attempting to make a connection will power up the networking hardware. If you are relying on Reachability (or NWPathMonitoraddition mine) saying something might be reachable, you are relying on something else (a background process) powering up the networking hardware [AFNetworking GitHub thread].

The solution is to use Adaptable Connectivity APIs. By opting in to this feature, we tell URLSession that it should wait for a connection to the server instead of failing a URLSessionTask because of a lack of connectivity.

All it takes to enable Adaptable Connectivity is to set waitsForConnectivity flag on URLSessionConfiguration:

let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
config.timeoutIntervalForResource = 300

let session = URLSession(configuration: config)

let url = URL(string: "https://www.example.com/")!

session.dataTask(with: url) { data, response, error in
    // ...
}

Now we are all set. If a device is online, HTTP requests will fire immediately. Otherwise, URLSession will wait until the device is connected to the Internet, and then fire a request.

By default, URLSession waits for the Internet connection for up to 7 days. If this is not the behavior you want, you could set timeoutIntervalForResource to a relevant value.

Enabling or Disabling App Features Based on Internet Connection

Same as with pre-flight checks, enabling or disabling app features based on an Internet connection is discouraged. The reason for this is that we cannot reliably check connectivity status via Reachability or NWPathMonitor:

  • The Wi-Fi signal could disappear after your app checks reachability, but before it connects [Networking Overview].
  • Different hosts may be reachable over different interfaces. You cannot trust a reachability check for one host to be valid for a different host [Networking Overview].
  • Different IP addresses for the same host may be reachable over different interfaces [Networking Overview].
  • […] You can no longer assume that Internet connectivity, once established, will remain established, or that bandwidth will never increase or decrease […] [Apple’s Networking Overview].
  • Wi-Fi Assist can switch you automatically to cellular if you have a poor Wi-Fi connection so that you can continue using the Internet.
  • Your device might think it’s on Wi-Fi, but when it tries to use it, it turns out not working [Advances in Networking, Part 2, 57:15].

As highlighted in Advances in Networking, instead of disabling app features and letting users hope for the best, we should indicate on the UI that the current connectivity status is insufficient, without blocking any actions. For example, Safari provides a refresh button on a web page which failed to load due to the lack of connectivity since automatic refresh is unreliable:

How to Check Internet Connection on iOS with Swift

Let’s lay out the idiomatic approach to working with an insufficient Internet connection.

Given that you opted into Adaptable Connectivity, you can now handle connectivity updates via URLSessionDelegate. The following methods are called once per URLSessionTask, and are a good place to indicate the status on the UI, e.g., by presenting an offline mode or a cellular-only mode:

class NetworkingHandler: NSObject, URLSessionDelegate {
    func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
        // Indicate network status, e.g., offline mode
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest: URLRequest, completionHandler: (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
        // Indicate network status, e.g., back to online
    }
}

let session = URLSession(configuration: config, delegate: NetworkingHandler(), delegateQueue: .main)

In case you haven’t opted into Adaptable Connectivity, you should use NWPathMonitor to receive network status updates. First, create a monitor instance. Make sure that you store a strong reference to it, e.g., by having it as a property in AppDelegate:

import Network

let monitor = NWPathMonitor()
monitor.start(queue: .global()) // Deliver updates on the background queue

Then use pathUpdateHandler to handle status changes:

monitor.pathUpdateHandler = { path in
    if path.status == .satisfied {
        // Indicate network status, e.g., back to online
    } else {
        // Indicate network status, e.g., offline mode
    }
}

The benefits of using NWPathMonitor over SCNetworkReachability is that it offers higher-level APIs, and provides more information about the network connection. We no longer need to create a network socket, and listen for its changes – all using low-level C APIs and pointers.

Once we receive a network status update – either via URLSessionDelegate or NWPathMonitor – we can diagnose the current connection via NWPathMonitor:

if monitor.currentPath.status == .satisfied {
    // Connected
} else {
    // No connection
}

if monitor.currentPath.isExpensive { 
    // Using an expensive interface, such as Cellular or a Personal Hotspot
}

if monitor.currentPath.isConstrained {
    // Using Low Data Mode
}

There is still a chance that a connection will drop after it has been established. In such a case, you will receive an NSURLErrorNetworkConnectionLost error in URLSessionTask’s completion handler. This is a good place to diagnose connectivity status using NWPathMonitor to handle the error appropriately.

Attaching Constraints to Network Operations

Another common scenario is to constrain network operations based on Internet connection status. E.g., disable video autoplay, or automatic downloads, or high quality streaming via cellular.

Networking Overview suggests that we should not use Reachability to decide whether a network operation needs to be constrained or not:

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

According to Advances in Networking, Part 1, the preferred way of minimizing data usage is by adopting Low Data Mode. Its purpose is to restrict background network use and save cellular and Wi-Fi usage. Since Low Data Mode is a system-wide preference, it gives control into users’ hands.

We can enable Low Data Mode on a URLSession level:

var config = URLSessionConfiguration.default
config.allowsConstrainedNetworkAccess = false

let session = URLSession(configuration: config)

let url = URL(string: "https://www.example.com/")!

session.dataTask(with: url) { data, response, error in
    // ...
}

Or on a per-request basis:

let url = URL(string: "https://www.example.com/")!
var request = URLRequest(url: url)
request.allowsConstrainedNetworkAccess = false

URLSession.shared.dataTask(with: request) { data, response, error in
    // ...
}

In this case, when there are no available networks without constraints, HTTP requests will fail with URLError whose networkUnavailableReason is set to .constrained.

Here are some examples of how built-in iOS apps adapt Low Data Mode [1]:

  • App Store: Video autoplay, automatic updates, and automatic downloads are turned off.
  • Music: Automatic downloads and high-quality streaming are turned off.
  • Podcasts: The frequency of feed updates is limited, and episodes are downloaded only on Wi-Fi.
  • News: Article prefetching is turned off.
  • iCloud: Updates are paused, and automatic backups and iCloud Photos updates are turned off.
  • FaceTime: Video bitrate is optimized for lower bandwidth.

If Low Data Mode is not what you need, you can use the following properties to make decisions about what to do on a given network:

  • allowsExpensiveNetworkAccess
  • allowsCellularAccess

The preferred one is allowsExpensiveNetworkAccess since it is more future-proof than allowsCellularAccess.

Summary

Let’s summarize the list of best practices that we’ve discussed:

  • Avoid pre-flight checks. Instead, opt into Adaptable Connectivity APIs.
  • Do not enable or disable app features based on an Internet connection. Instead, indicate connectivity status on UI, and diagnose network errors.
  • Do not second-guess your users by prevent connections from being sent over a cellular network. Instead, adopt Low Data Mode.

Thanks for reading!

If you enjoyed this post, be sure to follow me on Twitter to not miss any new content.

Vadim Bulavin

Creator of Yet Another Swift Blog. Senior iOS Engineer at Pluto TV. Coding for fun since 2008, for food since 2012.

Follow