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第一次使用到本地网络功能时,会有如下提示
  • 接收端需要扫描发送端的二维码进行连接,需要在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
    20
    func 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
      15
      var 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)
      }

发送数据

  • iOS14在开启广播的前提下才能检测是否有本地网络权限,所以必须先调用如下方法进行开启:
    1
    2
    3
    4
    5
    6
    7
    fileprivate 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
    11
    requestLocalNetworkAuthorization { [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
    9
    func 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
    9
    func 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
    7
    func 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
      9
      func 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
      func 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方式