Skip to main content

How to Detect Network Connectivity in iOS

How to Detect Network Connectivity in iOS

One topic that I have seen come up a lot over the last couple of years is how to detect internet connectivity in an iOS or macOS application.  This is a topic that really interests me so I thought I would take some time and write a tutorial on how I and Apple recommend that client applications determine network connectivity.  To start, I thought I would provide some context on where this network connectivity model might be applicable.  The main use case is in a traditional client to server communication setting where a client is sending either a GET or POST request to a server, and the server responding back with a response code or some JSON data.  This use case is typically seen in most mobile client applications today - especially those running on iOS.  For alternative forms of connectivity detection that do not fit this traditional use case, like those used in real time communication, a more advanced approach will need to be considered.  For more on this, see my NetworkConnectivity repository here.  

 

The Network Connectivity Model 🚀

The first piece I want to cover in this tutorial is the strategy behind the network connectivity model.  In a client to server communication setting, the best practice that I and Apple both recommend is to send a network request and let that network request tell you if the network is reachable or not.  So, what does that mean?  That means if the network is reachable by the client sending the HTTP request, then the network request will go out, and at some point a server response should come back to the client application.  If the network is not reachable, then the request should immediately fail and you as the application developer can update the client side user experience as the client sees fit.  

 

Creating a View Controller to Provide Connectivity Feedback 👨‍💻

Now that the network connectivity model has been addressed, let's take a look at how to implement this model in an iOS application.  The first step is to create a View Controller to display the connectivity feedback to the user.  This is illustrated with the basic interface I setup with a storyboard below:

View Controller Storyboard

Next, I setup the code below to wire up this storyboard to the View Controller.  Let's take a look at what is happening; first I wired up outlets for connectivityStatus and connectivityContainerInstruction as UIlabels.  These UIlabels will be used to provide information on the network connectivity status delivered by the HTTP request.  Next, I setup the View Controller to conform to the NetworkManagerDelegate.  This means the View Controller assigns itself as conforming class and an extension of the View Controller is setup to receive calls from the NetworkManagerDelegate.  More about the NetworkManagerDelegate in a bit.

Here is the ViewController.swift code:

import UIKit
 
class ViewController: UIViewController {
 
    // MARK: - IBOutlets
    @IBOutlet private weak var connectivityStatusContainer: UIView!
    @IBOutlet private weak var connectivityStatus: UILabel! // Connectivity Status Label
 
    @IBOutlet private weak var requestContainer: UIView!
    @IBOutlet private weak var requestContainerInstruction: UILabel! // Connectivity Instructions
    @IBOutlet private weak var requestContainerButton: UIButton! // Request Data Button
 
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        // Add outline to our views for visibility
        connectivityStatusContainer.addShadow()
        requestContainer.addShadow()
 
        // Instruct the NetworkManagerDelegate that ViewController conforms to the protocol.
        NetworkManager.shared.delegate = self
 
        // Set the initial connectivityStatus before a request is made.
        connectivityStatus.text = "Connection Status: Unknown"
    }
 
 
    @IBAction private func requestDataTapped() {  // IBAction for Request Data Button
        connectivityStatus.text = "Connection Status: Checking"
        NetworkManager.shared.getRequest()
    }
}
 
extension ViewController: NetworkManagerDelegate {
    // Success response that uses the response tuple to set the connectivity status and instruction.
    func networkFinishedWithData(response: (String, String, [String : AnyObject])) {
        DispatchQueue.main.async { [weak self] in
            self?.connectivityStatus.text = "Connection Status: \(response.0)"
            self?.requestContainerInstruction.text = response.1
        }
    }
    // Error response that uses the response tuple to set the connectivity status and instruction.
    func networkFinishedWithError(response: (String, String, [String : AnyObject])) {
        DispatchQueue.main.async { [weak self] in
            self?.connectivityStatus.text = "Connection Status: \(response.0)"
            self?.requestContainerInstruction.text = response.1
        }
    }
 
}

Here is the NetworkManagerDelegate.swift protocol code:

import Foundation
 
 
protocol NetworkManagerDelegate: class {
 
    func networkFinishedWithData(response: (String, String, [String: AnyObject]))
    func networkFinishedWithError(response: (String, String, [String: AnyObject]))
}

 

Creating a Network Manager 📡

Now that the View Controller and NetworkManagerDelegate are setup, let's take a looks at the NetworkManager singleton.  This is a statically shared object in our application that handles setting up, sending, and receiving network requests.  The NetworkManager class has an extension called NetworkManager+Requests.  This extension is used to separate concerns by isolating any GET and POST request logic attached to the NetworkManager.   NOTE: In this tutorial the code is concentrated on sending the request as opposed to processing the data received by the network response in any model objects.  

In the NetworkManager I have pointed out three main pieces of functionality:

  1. Request Setup or Execution. Used to setup a new URLSession and execute the dataTask().

  2. Response Error Processing.  Used to process network errors and report back to the View Controller.  Notice that any error passed back from the dataTask or any status code that is equal to or greater than 500 is processed as an error. 

  3. Response Success Processing. Used to process a network success and report back to the View Controller.  This normally would be where network data is parsed into a model object, but in this case the NetworkManagerDelegate is reporting back to the View Controller the connectivity status.

import Foundation
 
 
 
class NetworkManager: NSObject {
 
 
    //
    // MARK: - Static Class Constants
    //
    static let shared = NetworkManager()
 
    //
    // MARK: - Variables and Properties
    //
    weak var delegate: NetworkManagerDelegate?
 
    //
    // MARK: - Public constants
    //
    public let urlConfiguration = URLSessionConfiguration.default  // (1) Request Setup or Execution
    public let getSampleURL = "https://httpbin.org/get?arg1=1&arg2=2" // (1) Request Setup or Execution
 
}
 
 
//
// MARK: - Network Manager Extension for Single Requests
//
extension NetworkManager {
 
 
    func getRequest() {
 
        guard let getURL = URL(string: getSampleURL) else {
            return
        }
 
        var getRequest = URLRequest(url: getURL)  // (1) Request Setup or Execution
        getRequest.httpMethod = "GET"
 
        let urlSession = URLSession(configuration: urlConfiguration) // (1) Request Setup or Execution
 
        // Send the GET dataTask here
        let getRequestTask = urlSession.dataTask(with: getRequest, completionHandler: { [weak self] (data, response, error) in
 
            // Ensure that an error is not present, otherwise, return the error along with the message
            if let networkError = error {  // (2) Response Error Processing
                self?.delegate?.networkFinishedWithError(response: NetworkResponse.getNetworkResonse(networkResponse: .networkError,
                                                                                                    error: networkError))
                return
            }
 
            // Perform the JSONSerialization and send back data as long as the statusCode is good
            do {
                if let networkData = data, networkData.count > 0,
                    let networkJSON = try JSONSerialization.jsonObject(with: networkData, options: []) as? [String: AnyObject] {
 
                    // Send back the network Server error response here
                    if let httpResponse = response as? HTTPURLResponse,
                        httpResponse.statusCode >= 500 {
                        // (2) Response Error Processing
                        let error: NSError = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Sever Error"])
                        self?.delegate?.networkFinishedWithError(response: NetworkResponse.getNetworkResponse(networkResponse: .networkError,
                                                                                                            data: networkJSON,
                                                                                                            error: error))
                        return
                    }
 
                    // (3) Response Success Processing
                    self?.delegate?.networkFinishedWithData(response: NetworkResponse.getNetworkResponse(networkResponse: .networkData,
                                                                                                       data: networkJSON,
                                                                                                       error: error))
                    return
 
                }
            } catch _ as NSError {
                // Do something with the error here.
            }
            // (2) Response Error Processing
            // Send back a failure.  In this case the JSON would fail to parse to [String: AnyObject]
            self?.delegate?.networkFinishedWithError(response: NetworkResponse.getNetworkResponse(networkResponse: .networkError,
                                                                                                error: error))
 
        })
        getRequestTask.resume() // (1) Request Setup or Execution
 
    }
 
}

 

Creating the Feedback Loop for the View Controller 🧩

Now that the View Controller is in place and the NetworkManager is setup to send the request, it's time to close the feedback loop by processing the error or success messages with the NetworkResponse enum.  The NetworkResponse enum was created as a way standardize a network response to the View Controller.  Notice the getNetworkResponse function that takes 3 arguments; the networkResponse case is to direct how the instructions and status is sent to the View Controller, the network data is just passed through, and the network error is used for closer examination on how to set the status and instructions.  Once the getNetworkResponse interprets the passed in data it returns a 3-value tuple that contains the status, the instructions, and the network data for the View Controller to report information to the user.

Taking a closer look at the .networkData case from number 3 above, notice that when this case is sent to the getNetworkData function the status is set to "Online", the instruction is set to "Based upon your last network request, you connectivity is good," and the data is passed through.  This means that the network request was successfully sent by the NetworkManager and the connectivity status can be considered online.  Next, let's take a looks at the .networkError case.  When this case is interpreted the localizedDescription is set as the errorInstruction and the connectionStatus is set to "Online" by default.  This represents a general server error but the device is still in a network connected state because the request did leave and respond back to the application.  In the networkError case where URLSession provides an error with a localizedDescription of "The Internet connection appears to be offline," and an error code of -1009.  To safely recognize the offline error state, the error code is extracted and if -1009 is the error code then the connectionStatus needs to be set to offline because iOS has let us know in this instance that the network appears to be in a non-reachable state.

import Foundation
 
 
enum NetworkResponse: String {
 
    case networkData
    case networkError
 
    // This is an example for the NetworkConnectivity Turorial.
    // In a real life situation, the enum could be further built out to display actual objects being passed back insted of [String: AnyObject].
    // This would also be an excellent opportunity to use Codables.
    static func getNetworkResponse(networkResponse: NetworkResponse, data: [String: AnyObject] = [:], error: Error?) -> (String, String, [String: AnyObject]) {
 
        switch networkResponse {
 
        case .networkData:
            return (
                "Online",
                "Based upon your last network request, you connectivity is good.",
                data
            )
        case .networkError:
 
            var connectionStatus = "Online"
            var errorInstuction = error?.localizedDescription ?? "Failed to Parse JSON"
 
            if let nsError = error as NSError? {
                let errorCode = nsError.code
                /// -1009 is the offline error code
                /// HTTP load failed (error code: -1009)
                if errorCode == -1009 {
                    connectionStatus = "Offline"
                    errorInstuction = "Please check your internet connection and try your request again."
                }
            }
            return (
                connectionStatus,
                errorInstuction,
                data
            )
 
        }
 
    }
}

Now that the feedback loop is closed, let's take a look at both states running together.  Like I mentioned above, if the device is offline the request should immediately fail and providing feedback to the user to "Please check you internet connection and try your request again." 

Full Result for Offline Behavior

 

If the device is online, the request should go out and respond back to the device letting it know that the device is online, or, "Based upon your last network request, you connectivity is good."

Full Result for Online Behavior

 

In Summary ⌛️

In summary, network connectivity is not binary state, but iOS and macOS do provide a very logical connectivity model when trying to represent the network connectivity status in a traditional client to server communication setting.  I have seen a lot of success in this model outside of external network factors like VPN and real time communication.  These cases should be handled differently when trying to determine device connectivity.  I hope you enjoyed reading this tutorial, all of the code from this tutorial is available on my Github here.  Please let me know if there are any questions, comments, or concerns by leaving a comment below. Thank you!  

Member for

3 years 9 months
Matt Eaton

Long time mobile team lead with a love for network engineering, security, IoT, oss, writing, wireless, and mobile.  Avid runner and determined health nut living in the greater Chicagoland area.

Comments

Hi Matt, this was a very good article on detecting network. Checking network errors when making network requests is a great way to detect when things go wrong.

However, two things I noticed in your code was
1) you were checking for 500 errors but I think anything that is not 200 may need to be displayed with a different feedback to the user. But since this is about checking for network connectivity, then 500 is the best.
2) you are comparing localizedDescription to "The Internet connection appears to be offline." which may not be a consistent message to check for considering localized languages, and that Apple dictates what this message and could change it at any time. I believe this error comes with a code which I would think would be more consistent to compare against.

Just my 2 cents.

Jon, thank you for the feedback.  The localized description point, about checking the actual string message, is a very good point.  This could change over time on Apple's end.  I think that warrants an update to the article.  Appreciate your thoughts and you taking the time to read the article!