iOS 网络框架:如何监测网络状态变化

start

原文:https://www.appcoda.com/network-framework/

翻译:by jesse


大家好,欢迎观临!所有与服务器交换数据的应用始终必须了解的一件事情:他们是否连上Intenet。离线时,通常需要改变用户体验并更新用户界面,来反映应用程序无法执行基于网络的操作。此外,即使应用程序连接到Internet,了解连接类型(如Wifi或者蜂窝网络)也是非常的有帮助。没有人愿意在不知道的情况下应用程通过用蜂窝网络去拉取大量的数据,因为这会导致用户移动数据计划的额外成本。用户应该能够根据自己的意愿开启或者关闭的功能。

值得庆幸的是,获得所需信息已确定以上内容已经变得非常的简单,因为Apple在iOS 12 中提供了Network 框架。有了它,获取网络状态和收到变更的通知是一个标准和简单的过程,我们将在今天这篇文章中看到详细信息。在iOS 12 之前,要获取必要的网络信息并监听它的变化是要基于SCNetworkReachability API,是一件繁琐的任务,更像是C语言的解决方案。多年来,一些自定义实现使得使用SCNetworkReachability变的更容易,但是随着Network框架推出将近一年,它将很快成为历史。

在这篇文章中,我们不仅能看到Network框架的细节和如何使用它去监测网络状态的变化,而且在接下来的部分我们还将通过基于我们将要执行的实现创建小型的自定义框架作为一个可以复用的组件。锦上添花我们将要把自定义框架制作为cocoaPods分发并展示它所需要的所有集成步骤,感兴趣吗?

示例项目概述

今天我们将运行一个简单的项目就是在表示图里展示网络相关信息。具体地说,应用程序将显示设备是否连接上任何的网络接口(Wi-Fi,蜂窝,等),接口类型是什么,所有可获取的接口类型,考虑是否使用当前接口作为昂贵的操作,我们将在接下来讨论更多关于所以这些事情。

png

以上所述都是我们预先工作的结果,接下来我们将实现一个自定义的类并命名为NetStatus,在这个类里我们利用iOS SDK中的etwork框架,创建一个自定义的,容易使用的API,可以从那里集成到任意应用程序中去。我们使用Network框架并不需要做太多的事情,所以我们实现的任务将很快可以完成。这是一个好消息,因为我们将有机会基于NetStatus类创建一个小型自定义框架,以便我们可以在各种iOS项目中轻松使用它。

这里有一个启动项目可以让你下载,初步过工作已经完成。在文章中某个地方将要求你保留项目的副本,因为我们将对项目进行重大的修改。而且你将会最终得到两个项目的变体:第一个将包含演示应用程序和NetStatus类作为一个项目,第二个将包含自定义框架和演示应用程序作为不同的项目,框架将使用本地pod集成到演示应用程序中。

因此,享受你的阅读并了解Network框架的基本使用,学习如何使得自己的开源Swift框架可以作为cocoaPods进行分发。

创建一个单例类

我们将从直接实现一个自定义类开始。通过这个类的方法和属性我们将创建和提供一个可复用的API接口使得Network框架使用起来更加容易。

我们的工作将在NetStatus.swift文件中进行,你将在启动项目中找到它,它当前是空的。我们将使用和文件名相同的类名:

1
2
3
class NetStatus {

}

在继续之前,我们要先引入Network框架才能够使用它的API接口:

1
import Network

NetStatus是一个单例类,无需在需要使用它的时候创建实例。依据单例模式有两个要求:第一个,我们必须在类里中初始化一个static类型的共享实例:

1
static let shared = NetStatus()

第二步,我们必须创建一个私有的初始化方法:

1
2
3
private init() {

}

很好,目前为止类里的代码如下:

1
2
3
4
5
6
7
class NetStatus {
static let shared = NetStatus()

private init() {

}
}

我们将在此类中定义的任何属性和方法将使用项目周围任何位置的共享实例(例如NetStatus.shared.XXX)进行访问。

Network框架的确提供了更简单的监听网络状态变化的方法通过调用NWPathMonitor的类。我们来声明一个这个类型的属性:

1
var monitor : NWPathMonitor?

这个属性可能是最重要的属性,因为几乎所有关于网络框架的内容都将通过它完成。

另外,我们要声明一个标识来标示这个类是否正在监听网络状态变化:

1
var isMonitoring = false

很显然初始值我们设置为false,因为还没进行监控。

让我们声明几个处理回调的闭包,他们将在如下情况被调用:

  • 类开始监听网络变化。
  • 类停止监听网络变化。
  • 发生网络状态变化。

代码如下:

1
2
3
4
5
var didStarMonitoringHandler: (() -> Void)?

var didStopMonitoringHandler: (() -> Void)?

var netStatusChangeHandler: (() -> Void)?

其他任何实体类使用NetStatus类实现以上闭包回调将会收到网络状态变化,监听网络开始和结束的通知。

注:我们也能够使用代理模式或者是用NotificationCenter的通知模式来替代闭包模式进行实体类和 NetStatus类间的通信。然而,闭包是最快,最简单,最直接的信息发送方式。

开始监听网络状态改变

为了从Network框架中接收网络状态变化,我们必须要监听他们,为了达到监听的目的我们在类里定义第一个方法:

1
2
3
func startMonitoring() {

}

首先我们要做一个检查,通过查看isMonitoring属性的值来确定是否要开启监听。只有当没有正在监听存在我们才继续执行:

1
guard !isMonitoring else { return }

我们现在初始化 monitor属性:

1
monitor = NWPathMonitor()

但是,这还不足以触发实际的监控过程。 我们必须调用一个名为start(queue :)的方法,并将调度队列作为参数传递,在该参数中将执行监视。 重点要理解观察网络变化必须在后台线程上运行,而不是在主线程上运行。 那么,让我们创建一个调度队列,并调用该方法:

1
2
let queue = DispathchQueue(label: "NetStatus_Monitor")
monitor?.start(queue: queue)

接下来使NetStatus能够通知其调用者,当有任何的网络状态改变,如从Wifi网络切换到蜂窝网络,或者是完全断开网络。这是通过pathUpdateHandler属性完成的,它是一个闭包,返回一个NWPath对象,有关的接口更新信息,设备是否连接等等。不过这里我们并不直接使用它,而是调用之前已经声明好的netStatusChangeHandler闭包来替代:

1
2
3
monitor?.pathUpdateHandler = { _ in
self.netStatusChangeHandler?()
}

最后,我们需要更新isMonitoring属性标识,并且调用didStartMonitoringHandler闭包:

1
2
isMonitoring = true
didStartMonitoringHandler?()

startMonitoring()方法现在已经准备就绪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func startMonitoring() {
guard !isMonitoring else { return }

monitor = NWPathMonitor()
let queue = DispathchQueue(label: "NetStatus_Monitor")
monitor?.start(queue: queue)

monitor?.pathUpdateHandler = { _ in
self.netStatusChangeHandler?()
}

isMonitoring = true
didStartMonitoringHandler?()
}

停止监测

并非应用程序所有模块都需要进行监测网络状态变化,所以关闭监听是非常重要的。当不需要网络监测时候停止网络监测能更好的回收资源。

让我们定义一个新的方法:

1
2
3
func stopMonitoring() {

}

首先我们要检查这个类当前是否正处于监听状态,并且monitor属性是否已经进行了初始化:

1
guard isMonitoring, let monitor = monitor else { return }

要停止监测只需要调用cancel()方法,如下所示:

1
monitor.cancel()

然后更新我们自定义的属性:

1
2
self.monitor = nil
isMonitoring = false

我们要记得去调用didStopMonitoringHandler闭包来通知那些调用这个类的任何一个实体对象:

1
didStopMonitoringHandler?()

这边就是stopMonitoring()方法的具体内容:

1
2
3
4
5
6
7
8
func stopMonitoring() {
guard isMonitoring, let monitor = monitor else { return }
monitor.cancel()
self.monitor = nil
isMonitoring = false
didStopMonitoringHandler?()

}

当你想要停止网络监测时候你调用以上的内容例如:NetStatus.shared.stopMonitoring()。然而如果不明确该在哪里调用会发生什么捏?

是的,在init()方法之后实现deinit方法,若监测尚未停止则在里面调用stopMonitoring()方法:

1
2
3
4
deinit {

stopMonitoring()
}

提供网络状态数据

能够开启和关闭监测网络状态只完成了一半的工作。另外一半是应用程序通常需要了解的重要的数据。通过我们的课程我们将轻松的获得和了解这些内容:

  • 应用程序是否连接上了接口(wifi,蜂窝网,等)。
  • 当前连接的接口类型是什么。
  • 应用程序在任意给定时刻可调用接口。
  • 是否使用特定接口类型被认为是昂贵的操作。

我们将通过只读属性实现以上内容。

确定设备是否连接

我们将开始通过创建一个布尔属性来标识设备是否连接上接口:

1
2
3
4
5
var isConnected: Bool {
guard let monitor = monitor else { return false }
return monitor.currentPath.status == .satisfied

}

如果 monitor属性为空,那么当前并未在监测网络状态变化,所以我们仅仅返回false。当monitor为空时是不能够确定当前设备是否连接接口。

相反情况下,我们检查monitor对象当前网络路径(currentPath属性)的status属性的值,如果该值等于satisfied则设备连接上了接口。了解更多关于status属性和它可能的值。当网络状态发生变化时候会自动更新currentPath属性,所以每次访问都会返回实际的数据。

获取当前接口类型

除了了解设备是否连接到网络,了解它连接到接口类型也是很有帮助的。使NetStatus类能够确定是否连接上wifi网络,蜂窝网,或是用有线连接(以太网用于桌面macOS系统的应用程序)等等,我们应该提供这些极为有用和必需的功能组成。

和之前一样,这里提供了另一个只读属性:

1
2
3
4
5
6
7
var interfaceType: NWInterface.InterfaceType? {
guard let monitor = monitor else { return nil}

return monitor.currentPath.availableInterfaces.filter{
monitor.currentPath.usesInterfaceType($0.type)
}.first?.type
}

再次强调,只有当monitor属性不为空的时候才能继续执行是很重要的。要获取当前路径支持所有可用的接口,currentPath为我们提供availableInterfaces 属性,是个NWInterface对象集合。通过提供的usesInterfaceType(_:)方法更加容易的确定当前是否连接上指定的接口。上面filter方法返回一个新集合,该集合包含正在请求的被使用的接口。我们正在访问它的第一个(也是唯一的)元素,我们返回type属性,它是实际的接口类型(例如wifi)。 我建议你检查除类型之外的其他提供的属性,并使用它们与我们刚刚在这里做的类似。

获取可使用的接口类型

在上面我们使用availableInterfaces属性来获取当前网络路径的所有可用接口。 此集合中的接口类型可能对将使用NetStatus类的其他实体有用,那么为什么不使它也可用呢?

根据相同的模式,我们再写一个属性:

1
2
3
4
var availableInterfacesTypes: [NWInterface.InterfaceType]? {
guard let monitor = monitor else { return nil}
return monitor.currentPath.availableInterfaces.map { $0.type }
}

Swift中的高阶函数(如map)(或我们之前使用的filter)在此类情况下显着减少了所需的代码。 第二行的作用是基于原始的NWInterface集合创建一个新的NWInterface.InterfaceType对象数组,并为我们提供我们正在寻找的可用接口类型。

检查昂贵的接口

根据苹果文档,某些接口使用是否为昂贵的。Network框架有个标识isExpensive属性使得判断更加方便。我们将创建一个相识的名字属性并返回原始isExpensive属性的值。

1
2
3
var isExpensive: Bool {
return monitor?.currentPath.isExpensive ?? false
}

注意这里monitor属性为空的话,仅返回false。

查看Network框架的运行效果

我们已经完成主要的工作,是时候让NetStatus类执行并从Network框架中获得预期的数据。为了实现效果,我们切换到ViewController.swift文件,部分的实例代码和视图UI已经实现。它包含一个列表和一个开启和关闭的按钮(命名 monitorButton)。

列表视图包含三个部分:

* 第一部分只包含一行,它标识连接的状态(设备是否连接接口)。
* 第二部分罗列出所有可供设备连接的接口类型,另外,还将显示当前被使用的接口类型是哪个。
* 第三部分也是最后一部分,显示当前连接的接口是否属于昂贵。

在继续之前有一点要注意:我建议你在真机上运行实例程序而要用模拟去运行。在真实的环境工作下会有很大的差异,如你你连接网络是使用以太网模拟器运行的话很可能被误导。

所以,我们继续之前的操作,找到ViewController.swift文件里数据源方法tableView(_:numberOfRowsInSection:)。这里没部分的行数必需要设置,当前第二部分返回的行数为0:

1
2
3
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section != 1 ? 1 : 0
}

由于我们没有使用NetStatus类实现是无法知道有多少个可供设备连接的接口,现在我们准备更新上面的代码返回可使用接口的数量如下:

1
2
3
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section != 1 ? 1 : NetStatus.shared.availableInterfacesTypes?.count ?? 0
}

请注意如果某些原因导致availableInterfacesTypes为nil我们需要提供默认值,使用(??)合并操作,这种情况下不需要显示行数。

让我前往数据源方法tableView(_:cellForRowAt:).除了出列和返回单元格,这里不会发生其他任何事情。我们将通过添加代码做些修改,在合适的部分显示合适的数据。我们将用indexPath.section为条件声明一个switch条件语句开始。

1
2
3
switch indexPath.section {

}

我已经说过第一部分我们将显示设备是否连接上接口,所以我们使用NetStatus类的isConnected属性。我们将用文字和图片来做到这一点:

1
2
3
case 0:
cell.label?.text = NetStatus.shared.isConnected ? "Connected" : "Not Connected"
cell.indicationImageView.image = UIImage(named: "checkmark") : UIImage(named: "delete")

在第二部分我们将列出所有可用的接口类型,而且我们将标记当前连接的设备都接口类型。为了实现这个我们将使用availableInterfacesTypesinterfaceType两个属性来各自实现。

1
2
3
4
5
6
7
8
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
}
}

最后,我们想要表示当前连接的接口是否昂贵,所以最后一部分如下代码:

1
2
cell.label?.text = NetStatus.shared.isExpensive ? "Expensive" : "Not Expensive"
cell.indicationImageView.image = NetStatus.shared.isExpensive ? UIImage(named: "warning") : nil

我们还需要实现default条件情况:

1
default: ()

那么数据源方法tableView(_:cellForRowAt:)将变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
}

列表的配置已经完成,但还有些内容需要添加。我们接下来通过 toggleMonitoring(_:)方法来开启或者关闭网络状态的监测。这很简单:

1
2
3
4
5
6
7
@IBAction func toggleMonitoring(_ sender: Any) {
if !NetStatus.shared.isMonitoring {
NetStatus.shared.startMonitoring()
} else {
NetStatus.shared.stopMonitoring()
}
}

最后,我们在ViewDidLoad()方法中实现这个闭包,使得ViewController类能够接受到网络状态改变的通知。

实例程序的需要,我敏只要执行当监测开始的时候去更新按钮的标题:

1
2
3
4
5
6
7
override func viewDidLoad() {
...

NetStatus.shared.didStartMonitoringHandler = { [unowned self] in
self.monitorButton.setTitle("Stop Monitoring", for: .normal)
}
}

我们还将同样执行监测停止对监听,并且要刷新列表的标识:

1
2
3
4
5
6
7
8
override func viewDidLoad() {
...

NetStatus.shared.didStopMonitoringHandler = { [unowned self] in
self.monitorButton.setTitle("Start Monitoring", for: .normal)
self.tableView.reloadData()
}
}

当网络状态改变的时候也需要重新加载列表:

1
2
3
4
5
6
7
8
9
override func viewDidLoad() {
...

NetStatus.shared.netStatusChangeHandler = {
DispatchQueue.main.async { [unowned self] in
self.tableView.reloadData()
}
}
}

值得注意的是使用异步调度队列使加载列表必需在主线程中进行,因为在后台线程监听网络状态的改变,视图界面的更新不允许发生在后台线程。

我们现在能够运行应用程序并看到网络状态改变时候属性的变化。从wifi切换到蜂窝网,再切回来,然后关闭网络,每次重新打开应用程序都能看到所有监听值的更新变化:

pic

结尾

今天我们通过一些不同的步骤,我们吧注意力转到除了Xcode以外其他的东西上。关注Network框架的本身,处理和获取网络更新变得是个简单的任务,通过使用NetStatus类使我们实现起来更加的方便。

为了使项目更加完善,我们将在下一篇文章中创建一个CocoaPod,这样我们就可以通过CocoaPods依赖管理器轻松集成和分发我们的框架,我们甚至可以创建一个GitHub存储库来托管它以进行远程访问。 我们会在细节到来时看到所有细节。 请继续关注我们即将推出的教程。

我希望你享受到今天这篇教程多内容,让你有学到一些新的东西。下期再见 👋 !

上面提到的实例,你可以在GitHub上下载这些实例代码

您的支持将鼓励我继续创作!