Hello folks and welcome! Beyond any doubt, all apps that exchange data with servers need to know one thing all the time: Whether they are connected to Internet or not. When being offline, it’s usually desirable to alter the user experience and update the user interface in order to reflect the incapability of the app to perform network-based operations. Moreover, even when an app is connected to Internet it’s always extremely useful to know the type of the connection, such as wifi or cellular. Nobody would like to be using an app that fetches a big load of data over a cellular network without knowing it, as that would lead to additional costs on the user’s mobile data plan. Users should be able to turn on and off such a functionality at their own will.
Thankfully, getting the required information to determine all the above has become really straightforward as Apple made available the Network framework in iOS 12. With it, getting network status and being notified about changes on it is a standard and easy process, and we’ll see its details in this post today. Before iOS 12, getting all the necessary network information and observing for changes was a cumbersome task that was based on SCNetworkReachability API, a more C-like solution. Over the years several custom implementations appeared to make working with SCNetworkReachability easy, but with the Network framework being present for almost a year now it will soon become history.
In this post not only will we see the details of the Network framework and how to use it in order to monitor for network changes, but we will also make it a reusable component by creating a small custom framework based on the implementation we’ll do in the next parts. The cherry on the cake is going to be the presentation of all steps required for making that custom framework available for distribution as a CocoaPod. Interesting enough?
Demo Project Overview
Today we are going to work on a relatively simple project where we’ll be displaying network related information on a table view. More particularly, the app will be showing whether the device is connected to any network interface (wifi, cellular, etc), what the interface type is, all available interface types, and whether using the current interface considered to be an expensive operation or not. We’ll discuss more about all these in the parts that are coming next.
All the above are meant to be the result of some prior work, and that’s going to be the implementation of a custom class which we’ll name NetStatus. In it we’ll utilize the Network framework from iOS SDK, and we’ll create a custom, easy to use API that could be integrated in any app from there on. There are not a lot of things that we can do with the Network framework, so our implementation tasks will finish soon. And that’s sort of a good news as we’ll have the chance to create a small custom framework based on the NetStatus class so we can use it easily in various iOS projects.
There is a starter project for you to download, where some preliminary work has been already done. At some point in the post you’ll be asked to keep a copy of that project as it is at the time, because we’re going to make significant changes to it. Also, you’ll get two variations of the final project: The first one will contain the demo application along with the NetStatus class as one project, the second will contain the custom framework and the demo application as different projects, with the framework to be integrated into the demo app using a local pod.
So, enjoy your reading and get ready to meet the Network framework initially. Then, learn how to make your own open-source Swift based framework distributable as a CocoaPod.
Creating a Singleton Class
We’ll start by going straight to the implementation of a custom class. Through the methods and properties of this class we will build and provide a reusable API that will make Network framework’s features easily accessible.
Our work will take place in the NetStatus.swift file, which you will find in the starter project and it’s currently empty. We’ll keep the same name for our class:
class NetStatus {
}
Before going any further, let’s import the Network framework so we can access its API:
import Network
NetStatus
is going to be a singleton class, so there will be no need to create instances of it whenever it’s necessary to use it. There are two requirements according to singleton pattern: First, we must initialize a static shared instace of the class inside the class:
static let shared = NetStatus()
Second, we must have a private initializer:
private init() {
}
Great! This is what we have so far:
class NetStatus {
static let shared = NetStatus()
private init() {
}
}
Any properties and methods that we’ll define in this class will be accessed using the shared instance (eg NetStatus.shared.XXX
) from anywhere around the project.
What Network framework actually offers is an easy way to observe for network changes through a class called NWPathMonitor
. Let’s declare a property of that type:
var monitor: NWPathMonitor?
This property is probably the most important one as almost everything regarding the Network framework will be done through it.
Additionally, let’s declare a flag which will be indicating if the class is currently monitoring for network status changes or not:
var isMonitoring = false
Obviously the initial value is set to false, since no monitoring is taking place.
Let’s declare a few callback handlers now (closures) that will be called when:
- Class starts monitoring for network changes.
- Class stops monitoring for network changes.
- Changes on the network status occur.
Here they are:
var didStartMonitoringHandler: (() -> Void)?
var didStopMonitoringHandler: (() -> Void)?
var netStatusChangeHandler: (() -> Void)?
Any other entity that will be using NetStatus
and implementing the above will be notified about network changes as well as when monitoring starts and stops.
Note: Instead of using closures as a means to communicate with the entity that will be using the NetStatus
class, we could have used either the delegation pattern or notifications through the NotificationCenter
. However, that’s the fastest, simplest, and probably the most direct way to forward messages from this class to other entities.
Start Monitoring For Network Status Changes
In order to receive network changes from the Network framework, we must start monitoring for them. For that purpose let’s define the first method in our class:
func startMonitoring() {
}
The first thing to do is to check if we’re monitoring already or not by examining the value of the isMonitoring
property. We will proceed if only no monitoring has already started:
guard !isMonitoring else { return }
We now can initialize the monitor
property:
monitor = NWPathMonitor()
However, that’s not enough to trigger the actual monitoring process. We must call a method called start(queue:)
and pass a dispatch queue as an argument in which monitoring will be executed. It is important to understand here that observing for network changes has to be running on a background thread and never on the main one. So, let’s create a dispatch queue and let’s call that method:
let queue = DispatchQueue(label: "NetStatus_Monitor")
monitor?.start(queue: queue)
The next step is to make NetStatus
capable of notifying its caller when any network changes occur, such as going from wifi to cellular interface or totally disconnecting from any interface. This is done with the pathUpdateHandler
property, a closure which returns a NWPath
object with information about the updated interface, whether device is connected or not, and more. However here we won’t use it directly. Instead we’ll call the netStatusChangeHandler
we declared earlier:
monitor?.pathUpdateHandler = { _ in
self.netStatusChangeHandler?()
}
Finally, let’s update the isMonitoring
flag as needed, and of course, to call the didStartMonitoringHandler
closure:
isMonitoring = true
didStartMonitoringHandler?()
startMonitoring()
method is now ready:
func startMonitoring() {
guard !isMonitoring else { return }
monitor = NWPathMonitor()
let queue = DispatchQueue(label: "NetStatus_Monitor")
monitor?.start(queue: queue)
monitor?.pathUpdateHandler = { _ in
self.netStatusChangeHandler?()
}
isMonitoring = true
didStartMonitoringHandler?()
}
Stop Monitoring
Not all parts of an app will require to monitor for network changes, so being able to stop monitoring is important. It’s also useful for saving resources when network monitoring isn’t necessary.
Let’s define a new method:
func stopMonitoring() {
}
At first we’ll check if the class is currently monitoring, and if the monitor
property has been actually initialized or not:
guard isMonitoring, let monitor = monitor else { return }
To stop monitoring, just call the cancel()
method as shown here:
monitor.cancel()
After that, update our custom properties as well:
self.monitor = nil
isMonitoring = false
We should not forget to call the didStopMonitoringHandler
for notifying any other entity using this class:
didStopMonitoringHandler?()
Here it is the stopMonitoring()
method:
func stopMonitoring() {
guard isMonitoring, let monitor = monitor else { return }
monitor.cancel()
self.monitor = nil
isMonitoring = false
didStopMonitoringHandler?()
}
Whenever you want to stop monitoring for changes in network status you’ll be calling the above as: NetStatus.shared.stopMonitoring()
. However, what should happen if that method does not get invoked explicitly?
Right after the init()
method implement the deinit
. Call the stopMonitoring()
in it in case monitoring has not been stopped already:
deinit {
stopMonitoring()
}
Providing Network Status Data
Being able to start and stop the monitoring of network changes is the half of the job we have to do. The other half regards important data that apps usually need to know about. We are going to make the following easily available and accessible through our class:
- Whether an app is connected or not to an interface (wifi, cellular, etc).
- What the current interface type is.
- The available interfaces at any given moment to the app.
- Whether using a specific interface type is considered to be an expensive operation or not.
We will implement all the above as get-only properties.
Determining If Device Is Connected
We will start by creating a boolean property which will indicate whether the device is connected to an interface or not:
var isConnected: Bool {
guard let monitor = monitor else { return false }
return monitor.currentPath.status == .satisfied
}
If the monitor
property is nil because the class is not currently monitoring for network changes, then we just return false. Whether there is a connection or not it’s something that cannot be determined while the monitor
is nil.
In the opposite case, we examine the value of the status
property of the current network path (currentPath
property) of the monitor object. If value equals to satisfied
then the device is connected to an interface. Find out more about the status
property and its possible values. currentPath
property is being updated automatically when network changes occur, so each time is being inquired it returns real data.
Getting The Current Interface Type
Apart from knowing if a device is connected to a network, it’s also useful to know the interface type that it’s connected to. Making NetStatus
class capable of determining if an app is connected to a wifi network, cellular network, if it’s using a wired connection (ethernet cable – that’s for desktop macOS apps), and so on, consists of another extremely useful and necessary feature we should include.
Like before, here’s another get-only property:
var interfaceType: NWInterface.InterfaceType? {
guard let monitor = monitor else { return nil }
return monitor.currentPath.availableInterfaces.filter {
monitor.currentPath.usesInterfaceType($0.type) }.first?.type
}
Once again it’s important to proceed if only the monitor
property has an actual value. To get all the available interfaces that the current path supports, currentPath
gives us the availableInterfaces
property, a collection of NWInterface
objects. It also makes it easy to determine if a specific interface is being currently used or not by providing the usesInterfaceType(_:)
method. filter
method above returns a new collection which contains only the interface that is being used at the moment of the request. We’re accessing its first (and only) element, and we return the type
property which is the actual interface type (e.g. wifi). I’d suggest you to examine other provided properties besides type
, and use them as well similarly to what we just did here.
Getting Available Interface Types
Right above we used the availableInterfaces
property to get all the available interfaces of the current network path. The types of interfaces in this collection could be useful to other entities that will be using NetStatus
class, so why not to make it available too?
Following the same pattern, let’s write one more property:
var availableInterfacesTypes: [NWInterface.InterfaceType]? {
guard let monitor = monitor else { return nil }
return monitor.currentPath.availableInterfaces.map { $0.type }
}
Higher order functions in Swift like map
(or filter
that we previously used) have minimized the required code significantly in cases like this one. What the second line does above is to create a new array of NWInterface.InterfaceType
objects based on the original NWInterface
collection, and give us that way the available interface types we are looking for.
Checking For Expensive Interfaces
According to Apple docs, certain interfaces (such as cellular) are considered as expensive to use. Network framework has a flag that makes it easy to check that (through the currentPath
property), and it’s called isExpensive
. We’ll create a similarly named property and we’ll return the value of the original isExpensive
.
var isExpensive: Bool {
return monitor?.currentPath.isExpensive ?? false
}
Notice that in case monitor
is nil, we just return false.
Seeing Network Framework In Action
Our main work has been pretty much done, so it’s time to put NetStatus
class in motion and get the expected readings from Network framework. For that purpose, let’s switch to the ViewController.swift file where part of the demo application and its UI has been already implemented. It contains a table view and a button (named monitorButton
) for starting and stopping monitoring.
Table view is going to contain three sections:
- The first section will contain one row only and it will indicate the connection status (whether the device is connected or not to an interface).
- The second section will be listing all available interface types to the device. In addition, it will be indicating visually the interface type that is being currently used.
- The third and last section will be showing whether the use of the current interface is expensive or not.
One note before we proceed: I recommend you to run the demo app in a real device and not in simulator. It’s totally different to see how everything works in real conditions, and simulator can be misleading especially if you’re connected to Internet using an ethernet cable.
So, back to action again, go to the tableView(_:numberOfRowsInSection:)
datasource method in the ViewController.swift file. Here is the place where the number of rows per section must be defined, and currently for the second section no rows are being returned:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section != 1 ? 1 : 0
}
That’s because we can’t tell how many interfaces are available to the device without having implemented the NetStatus
class. Now that we have it ready we can update the above and return the number of available interfaces as shown here:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section != 1 ? 1 : NetStatus.shared.availableInterfacesTypes?.count ?? 0
}
Note that if for some reason the availableInterfacesTypes
is nil we provide a default value using the nil coalescing operator (??); no rows at all in that case.
Let’s head to the tableView(_:cellForRowAt:)
datasource method. Except for dequeueing and returning the cell, nothing else happens here. We’ll change that by adding the code that will display the proper data to the proper sections. We’ll start with a switch
statement where we’ll go through indexPath.section
cases:
switch indexPath.section {
}
We said already that in the first section we’ll be displaying whether the device is connected or not to an interface, so we’ll use the isConnected
property of the NetStatus
class. We’ll do that both verbally and by using an image:
case 0:
cell.label?.text = NetStatus.shared.isConnected ? "Connected" : "Not Connected"
cell.indicationImageView.image = NetStatus.shared.isConnected ? UIImage(named: "checkmark") : UIImage(named: "delete")
In the second section we’ll list all available interface types. Also, we’ll mark the one that the device is currently connected to. For doing these we’ll make use of the availableInterfacesTypes
and interfaceType
properties respectively:
case 1:
if let interfaceType = NetStatus.shared.availableInterfacesTypes?[indexPath.row] {
cell.label?.text = "\(interfaceType)"
if let currentInterfaceType = NetStatus.shared.interfaceType {
cell.indicationImageView.image = (interfaceType == currentInterfaceType) ? UIImage(named: "checkmark") : nil
}
}
Lastly, we want to indicate if being connected to the current interface is expensive or not, so for the last section we have this:
case 2:
cell.label?.text = NetStatus.shared.isExpensive ? "Expensive" : "Not Expensive"
cell.indicationImageView.image = NetStatus.shared.isExpensive ? UIImage(named: "warning") : nil
We’ll need a default
case as well:
default: ()
The tableView(_:cellForRowAt:)
datasource method now has become like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "infoCell", for: indexPath) as! InfoCell
switch indexPath.section {
case 0:
cell.label?.text = NetStatus.shared.isConnected ? "Connected" : "Not Connected"
cell.indicationImageView.image = NetStatus.shared.isConnected ? UIImage(named: "checkmark") : UIImage(named: "delete")
case 1:
if let interfaceType = NetStatus.shared.availableInterfacesTypes?[indexPath.row] {
cell.label?.text = "\(interfaceType)"
if let currentInterfaceType = NetStatus.shared.interfaceType {
cell.indicationImageView.image = (interfaceType == currentInterfaceType) ? UIImage(named: "checkmark") : nil
}
}
case 2:
cell.label?.text = NetStatus.shared.isExpensive ? "Expensive" : "Not Expensive"
cell.indicationImageView.image = NetStatus.shared.isExpensive ? UIImage(named: "warning") : nil
default: ()
}
return cell
}
The table view has been configured but there are a couple more additions to make. Our next stop is the toggleMonitoring(_:)
IBAction method where we’ll be starting and stopping monitoring for changes in the network status. This is as simple as that:
@IBAction func toggleMonitoring(_ sender: Any) {
if !NetStatus.shared.isMonitoring {
NetStatus.shared.startMonitoring()
} else {
NetStatus.shared.stopMonitoring()
}
}
Finally, in the viewDidLoad()
method we’ll implement the closures that will let ViewController
class be notified when monitoring starts and stops, and when network changes occur.
For the needs of our demo app, the only action we’ll take when monitoring has started is to update the button’s title:
override func viewDidLoad() {
...
NetStatus.shared.didStartMonitoringHandler = { [unowned self] in
self.monitorButton.setTitle("Stop Monitoring", for: .normal)
}
}
We’ll do the same when monitoring stops, but we’ll also reload the table view for refreshing the visual indications:
override func viewDidLoad() {
...
NetStatus.shared.didStopMonitoringHandler = { [unowned self] in
self.monitorButton.setTitle("Start Monitoring", for: .normal)
self.tableView.reloadData()
}
}
Reloading the table view is what we’ll also do when network changes are taking place:
override func viewDidLoad() {
...
NetStatus.shared.netStatusChangeHandler = {
DispatchQueue.main.async { [unowned self] in
self.tableView.reloadData()
}
}
}
Note that using the above dispatch queue and reloading the table view on the main thread is mandatory, since monitoring changes are being observed in a background thread and UI changes are not allowed in background threads.
We can now run the app and see if changes to network are being reflected properly. Switch from wifi to cellular and back, turn networking off, and each time reopen the app to see all monitored values being updated:
Summary
Today we’ve gone through several different steps, and we’ve turned our attention to other stuff as well besides Xcode. Focusing on the Network framework itself, dealing with it and getting network updates as they happen is an easy task, and with the NetStatus class that we implemented we made it even simpler.
To make it even more “salty”, we’ll create a CocoaPod in the next post, so we can easily integrate and distribute our framework through the CocoaPods dependency manager, and we’ll even create a GitHub repository to host it for remote access. We’ll see everything in details as they come. Please stay tuned with our upcoming tutorial.
I hope you enjoyed the today’s post content, and that there’s something new that you’ve learnt here. See you next time 👋 !
For reference, you can download the demo project on GitHub.