iOS14本地网络适配
说在前面
- iOS14的适配集中在用户隐私和安全方面,包含相册、位置、本地网络、广告标识符、剪切板等隐私权限的适配。前段时间做了相册权限和本地网络权限的适配,相册权限的适配比较简单,不在本文讲解范围内,本文主要讲解本地网络适配。
- 苹果官方没有给出具体API查询本地网络权限是否开启,这给开发者带来了难度。本文采用间接的方式在需要本地网络权限的时候,提示是否开启权限,如果没有开启权限就不能执行相应的操作。本文讲解在NXPlayer这款App中怎样适配本地网络权限的。
- 点击底部
换机
标签->点击我是发送者
按钮->选好资料开始发送
.- 如果本地网络权限没有开启就会出现如下所示
- 如果本地网络权限已经开启,就不会有弹框提示,会直接出现一个二维码
- 如果本地网络权限没有开启就会出现如下所示
- 点击底部
换机
标签->点击我是接收者
按钮->进入二维码扫描界面,扫描刚才生成的二维码。- 如果本地网络权限没有开启就会出现如下所示
- 如果本地网络权限已经开启,就会直接连接上,跳转到资料传输界面。
- 如果本地网络权限没有开启就会出现如下所示
- MultipeerConnectivity是一个通过WiFi在近距离设备间建立连接、交换数据的框架,该App使用此框架编写接收和发送资料的代码。为了简单起见,需要单独写一个demo,项目名称叫LocalNetwork,演示是怎样检测到是否开启了本地网络权限的。
请求权限
- iOS14当App要使用Bonjour服务或者访问本地网络时,需要在Info.plist中配置两项
- 配置”NSBonjourServices”,格式为”_serviceType._tcp”
1
2
3
4<key>NSBonjourServices</key>
<array>
<string>_me-transferdata._tcp</string>
</array> - “权限使用说明“描述为啥需要这个权限,要说清楚,要不然过不了审核
1
Privacy - Local Network Usage Description String 请点击“好”以访问本地网络,发现、连接其他设备并进行数据传输
- 以上两项配置完成后,当App第一次使用到本地网络功能时,会有如下提示
- 配置”NSBonjourServices”,格式为”_serviceType._tcp”
- 接收端需要扫描发送端的二维码进行连接,需要在Info.plist中配置相机权限说明
1
Privacy - Camera Usage Description String 请点击“好”以访问相机,扫描二维码连接发送端设备
- iOS14之前默认都有本地网络访问权限,直接回调“true”;iOS14开始可以通过“DNSServiceBrowse”回调返回的错误码来判断是不是有权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20func requestLocalNetworkAuthorization(completion: @escaping ((_ granted: Bool)->())) {
if #available(iOS 14, *) {
_localNetworkCompletion = completion
let browseCallback: DNSServiceBrowseReply = { (_, flags, _, errorCode, name, regtype, domain, context) in
DispatchQueue.main.async {
_localNetworkCompletion?(errorCode != kDNSServiceErr_PolicyDenied)
}
}
DispatchQueue.global().async {
var browseRef: DNSServiceRef?
DNSServiceBrowse(&browseRef, 0, 0, "_me-transferdata._tcp", nil, browseCallback, nil)
DNSServiceProcessResult(browseRef);
DNSServiceRefDeallocate(browseRef);
}
} else {
DispatchQueue.main.async {
completion(true)
}
}
} - 本地网络功能需要打开Wifi开关
- 通过如下代码判断Wifi是否打开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var isWifiEnable: Bool {
var ifaddr: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddr) == 0 else { return false}
let cset = NSCountedSet()
while let addr = ifaddr {
let addrFamily = addr.pointee.ifa_addr.pointee.sa_family
if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) {
let name = String(cString: addr.pointee.ifa_name)
cset.add(name)
}
ifaddr = addr.pointee.ifa_next
}
freeifaddrs(ifaddr)
return cset.count(for: "awdl0") > 0
} - Wifi开关如果打开了就可以继续使用,没有打开就不能进行下一步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@IBAction func sendBtnClicked(_ sender: Any) {
print("我是发送者")
if !UIDevice.current.isWifiEnable {
presentWiFiAlterController()
return
}
navigationController?.pushViewController(SendController(), animated: true)
}
@IBAction func receiveBtnClicked(_ sender: Any) {
print("我是接收者")
if !UIDevice.current.isWifiEnable {
presentWiFiAlterController()
return
}
navigationController?.pushViewController(ReceiveController(), animated: true)
}
- 通过如下代码判断Wifi是否打开
发送数据
- iOS14在开启广播的前提下才能检测是否有本地网络权限,所以必须先调用如下方法进行开启:
1
2
3
4
5
6
7fileprivate lazy var advertiser: MCNearbyServiceAdvertiser = {
let advertiser = MCNearbyServiceAdvertiser(peer: dataCenter.peerID, discoveryInfo: nil, serviceType: dataCenter.serviceType)
advertiser.delegate = dataCenter
return advertiser
}()
advertiser.startAdvertisingPeer() - 广播开启后,请求是否有本地网络权限,有权限就直接显示二维码,没有权限就提示开启权限
1
2
3
4
5
6
7
8
9
10
11requestLocalNetworkAuthorization { [weak self] (granted) in
guard let weakSelf = self else { return }
if granted {
let data = NSKeyedArchiver.archivedData(withRootObject: weakSelf.dataCenter.peerID)
weakSelf.qrCodeView.image = UIImage.qrcodeImage(from:data.hexString, with:UIImage(named: "SendQRIcon"))
return
}
weakSelf.presentLocalNetworkAlterController {
weakSelf.navigationController?.popViewController(animated: true)
}
} - 对方扫描二维码连接成功后,发送一条信息给对方
1
2
3
4
5
6
7
8
9func session(peer peerID: MCPeerID, didChange state: TransferDataSessionState) {
if state == .connected {
dataCenter.send(type: .text, data: "我是发送者")
}
}
func session(didReceive data: Any, with type: TransferDataType) {
print(#function,"接收到消息--\(data)")
}
接收数据
- 指定扫描二维码区域
1
2
3
4
5
6
7
8
9/* 添加预览和蒙版 */
layer.insertSublayer(previewLayer, at: 0)
previewLayer.addSublayer(maskLayer)
maskLayer.setNeedsDisplay()
/* 启动扫描 */
startRunning()
/* 必须首先添加预览图层和启动扫描才能得到正确的扫描区域 */
let insertRect = previewLayer.metadataOutputRectConverted(fromLayerRect: containerView.frame)
output.rectOfInterest = insertRect - 扫描二维码得到数据后回调给控制器
1
2
3
4
5
6
7
8
9func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if metadataObjects.count > 0 {
/* 取出扫描到的数据 */
guard let readableObject = metadataObjects.last as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue else { return }
delegate?.scanViewMetadataOutput(with: stringValue)
stopRunning()
}
} - 接收控制器拿到扫描数据后,接档转换为peerID,然后进行连接
1
2
3
4
5
6
7func scanViewMetadataOutput(with stringValue: String) {
guard let hexData = stringValue.hexData,
let otherPeerID = NSKeyedUnarchiver.unarchiveObject(with: hexData) as? MCPeerID else { return }
self.performScanStop()
/* 进行连接 */
browser.invitePeer(otherPeerID, to: dataCenter.session, withContext: nil, timeout: 10)
} - 执行连接后,有两种情况:
- 连接成功,发送消息给对方
1
2
3
4
5
6
7
8
9func session(peer peerID: MCPeerID, didChange state: TransferDataSessionState) {
if state == .connected {
dataCenter.send(type: .text, data: "我是接收者")
}
}
func session(didReceive data: Any, with type: TransferDataType) {
print(#function,"接收到消息--\(data)")
} - 没有连接,丢失了连接。如果丢失了连接,并且满足以下两个条件就判断为没有开启本地网络权限,这个时候需要弹框提示开启权限
1
2
3
4
5
6
7
8
9func browser(didLostPeer peerID: MCPeerID) {
if #available(iOS 14, *) {
if !isScanFinished { return }
presentLocalNetworkAlterController { [weak self] in
guard let weakSelf = self else { return }
weakSelf.navigationController?.popViewController(animated: true)
}
}
}- iOS14以下本地网络权限默认为开启,所以条件之一为iOS14以下系统
- 丢失连接有很多情况,在发送方满足了所有的连接条件,接收方还是丢失了连接,这个是条件之二
- 连接成功,发送消息给对方
总结
- 此方法只适用于使用MultipeerConnectivity框架的App,对于其它框架中使用了本地网络功能的App不一定适用
- 官方没有Api来判断是否开启了本地网络权限,本文采用的是间接方式来判断是否卡其权限。
- 发送端采用
DNSService
方式 - 接收端采用
didLostPeer
方式
- 发送端采用