【问题标题】:iOS 14 How to trigger Local Network dialog and check user answer?iOS 14 如何触发本地网络对话框并检查用户答案?
【发布时间】:2023-03-31 11:40:01
【问题描述】:

我已经看过这个 Q/A What triggers,但这不是我想要的。我也读过这个Network privacy permission check,但没有答案。我还在这里搜索任何可以帮助我的方法或类:Network,但又没有运气了。

本地网络授权有一个新对话框,用户可以在其中允许/不允许“查找并连接到本地网络上的设备”。

但我正在努力寻找任何 API 来了解如何触发此弹出窗口以及如何检查是否授予访问权限(例如在 AVCapture 中,我可以检查 AVMediaType 的授权状态)。

谢谢!

【问题讨论】:

  • 目前还没有办法检查。但看起来有些事情正在进行中:您可以在这里关注进展:developer.apple.com/forums/thread/650648
  • 遗憾的是@valosip 的评论是正确的。另外我想知道为什么这个问题被否决了,因为这对于某些应用程序来说是一个巨大的问题。
  • @Teetz 是的,我也不明白。但是我已经开通了 DTS 技术支持,所以我会在收到 Apple 的回复后更新我的答案。

标签: ios swift networking


【解决方案1】:

我找到了一种方法来触发提示,接收用户选择的回调,并检测用户之前是否允许或拒绝了已经出现的提示。 为了触发权限,我们使用服务发现 API。当用户拒绝或之前拒绝时,我们会收到错误消息。 它不指示是否已授予权限,因此我们还发布了一个网络服务,如果已授予权限,则返回成功。 通过将 2 组合成一个组件,我们可以触发提示并获得批准或拒绝的指示:在我们收到网络服务的成功或服务发现的错误之前,我们假设权限仍在等待中。

import Foundation
import Network

@available(iOS 14.0, *)
public class LocalNetworkAuthorization: NSObject {
    private var browser: NWBrowser?
    private var netService: NetService?
    private var completion: ((Bool) -> Void)?
    
    public func requestAuthorization(completion: @escaping (Bool) -> Void) {
        self.completion = completion
        
        // Create parameters, and allow browsing over peer-to-peer link.
        let parameters = NWParameters()
        parameters.includePeerToPeer = true
        
        // Browse for a custom service type.
        let browser = NWBrowser(for: .bonjour(type: "_bonjour._tcp", domain: nil), using: parameters)
        self.browser = browser
        browser.stateUpdateHandler = { newState in
            switch newState {
            case .failed(let error):
                print(error.localizedDescription)
            case .ready, .cancelled:
                break
            case let .waiting(error):
                print("Local network permission has been denied: \(error)")
                self.reset()
                self.completion?(false)
            default:
                break
            }
        }
        
        self.netService = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
        self.netService?.delegate = self
        
        self.browser?.start(queue: .main)
        self.netService?.publish()
    }
    
    private func reset() {
        self.browser?.cancel()
        self.browser = nil
        self.netService?.stop()
        self.netService = nil
    }
}

@available(iOS 14.0, *)
extension LocalNetworkAuthorization : NetServiceDelegate {
    public func netServiceDidPublish(_ sender: NetService) {
        self.reset()
        print("Local network permission has been granted")
        completion?(true)
    }
}

使用方法:

  1. 将 LocalNetworkAuthorization 类添加到您的项目中
  2. 打开 .plist 文件并添加“_bonjour._tcp”、“_lnp._tcp.”作为“Bonjour 服务”下的值
  3. 调用 requestAuthorization() 触发提示或获取授权状态(如果已被批准/拒绝)

【讨论】:

    【解决方案2】:

    我确实打开了 DTS 请求并与 Apple 支持团队进行了转换。这是我在下面列出的一些重要部分。

    如何检查是否授予访问权限

    来自支持团队:

    要知道,没有这样的 API 来检查用户权限。

    来自支持团队:

    如果用户拒绝,则连接失败。它究竟是如何失败的 取决于您使用的网络 API 以及您使用该 API 的方式。

    • 默认情况下,连接将失败并显示NSURLErrorNotConnectedToInternet
    • 如果您在会话配置上设置waitsForConnectivity,请求将等待事情改善。在这种情况下,您将收到 -URLSession:taskIsWaitingForConnectivity: 委托回调到 告诉你这件事。如果用户改变主意并启用本地 网络访问,然后连接将通过。

    不幸的是,没有直接的方法来确定这种行为是否是 本地网络隐私限制或其他原因的结果 网络故障。


    如何触发此弹出窗口

    来自支持团队:

    这里的问题是本地网络权限警报是 由传出流量触发,您不会产生任何传出 交通。解决这个问题的唯一方法是生成一些虚拟传出 流量以触发此警报。

    我见过其他开发人员处于这种情况,并且没有 直接API触发本地网络权限告警相当 恼人的。我鼓励您提交有关此问题的错误。

    我一直在与本地网络隐私团队讨论这个问题 以及我们当前针对您所处情况的应用程序的建议——即, 想要接收广播但不发送任何本地网络流量 - 如下:

    • 系统应该更好地处理这个问题。我们将其作为错误rdar://problem/67975514 进行跟踪。这在 当前 iOS 14.2b1 版本,但您应该继续使用 iOS 进行测试 Beta 种子发布时。

    • 与此同时,您可以通过发送消息来强制显示本地网络隐私警报。我们特别建议您发送 与您尝试的消息大致相同的消息 接收,因此在您的情况下,这意味着发送 IPv4 UDP 广播。

    更新

    对于 iOS 14.2 - 收到入站流量 FIXED 的提示。因此,您不需要下面的示例来模拟流量来触发提示。


    这是用于虚拟传出流量模拟的类: example

    该流量永远不会离开 iOS 设备,因此,即使 接口处于休眠状态,它不会唤醒它。即使它真的醒来了 界面,成本微不足道,因为你没有这样做 一遍又一遍,只为了触发本地网络一次 隐私提醒。

    【讨论】:

      【解决方案3】:

      在我的情况下,它正在访问此变量以获取一些内部设备统计信息:

      ProcessInfo.processInfo.hostName
      

      访问此变量会导致出现警报。如果它不涵盖您的情况,也许您可​​以搜索源代码以获取本地网络/主机周围的一些参考。

      【讨论】:

      • 这很好,但不幸的是它没有回调......但它确实会导致弹出窗口出现。
      【解决方案4】:

      Apple 已(2020 年 9 月下旬)发布了一个 Local Network Privacy FAQ 来回答这个问题,尽管看起来可能会进行进一步的更改以使其更容易。

      SwiftObjective-C 代码示例说明如何通过变通方法触发提示:

      目前没有办法显式触发本地网络 隐私警报 (r. 69157424)。但是,您可以隐含地提出它 通过向本地网络地址发送虚拟流量。下面的代码 显示了一种方法来做到这一点。它找到所有 IPv4 和 IPv6 地址 与支持广播的网络接口相关联并发送 UDP 数据报到每个。这应该会触发本地网络隐私 警报,假设尚未为您的应用显示警报。

      至于如何查看结果,请留意this FAQ answer,上面写着:

      如果您的目标是使用 NWConnection 然后,从 iOS 14.2 beta 开始,您可以使用 unsatisfied reason 属性。

      【讨论】:

      • 使用 unsatisfied reason 似乎是最好的方法,直到引入一个 api 来检查结果。很想得到一个工作的例子。我发现的最好的东西是 - here 并没有完全帮助我
      【解决方案5】:

      因为没有 API 可以直接返回您的本地网络访问状态,您可以使用下一个方法发布您的 Bonjour 服务,并且如果您的应用程序已经设置了对本地网络的访问权限(例如在应用程序启动时),它会返回正确的结果。该方法也会导致警报出现,但在您选择任何按钮之前会返回false,因此为了获得正确的结果,您应该将此检查设置为applicationDidBecomeActive,它会在本地网络警报消失并返回后给出正确的状态到您的应用。

      class getLocalNetworkAccessState : NSObject {
          var service: NetService
          var denied: DispatchWorkItem?
          var completion: ((Bool) -> Void)
          
          @discardableResult
          init(completion: @escaping (Bool) -> Void) {
              self.completion = completion
              
              service = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
              
              super.init()
              
              denied = DispatchWorkItem {
                  self.completion(false)
                  self.service.stop()
                  self.denied = nil
              }
              DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: denied!)
              
              service.delegate = self
              self.service.publish()
          }
      }
      
      extension getLocalNetworkAccessState : NetServiceDelegate {
          
          func netServiceDidPublish(_ sender: NetService) {
              denied?.cancel()
              denied = nil
      
              completion(true)
          }
          
          func netService(_ sender: NetService, didNotPublish errorDict: [String : NSNumber]) {
              print("Error: \(errorDict)")
          }
      }
      

      使用方法:

      getLocalNetworkAccessState { granted in
          print(granted ? "granted" : "denied")
      }
      

      注意:不要忘记设置NSLocalNetworkUsageDescription 并添加“_lnp._tcp”。到 Info.plist 中的NSBonjourServices

      更新

      第二种方法与上面的代码类似,但可以通过检查应用程序状态等待用户的回答,然后返回本地网络隐私的有效访问状态:

      class LocalNetworkPrivacy : NSObject {
          let service: NetService
      
          var completion: ((Bool) -> Void)?
          var timer: Timer?
          var publishing = false
          
          override init() {
              service = .init(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
              super.init()
          }
          
          @objc
          func checkAccessState(completion: @escaping (Bool) -> Void) {
              self.completion = completion
              
              timer = .scheduledTimer(withTimeInterval: 2, repeats: true, block: { timer in
                  guard UIApplication.shared.applicationState == .active else {
                      return
                  }
                  
                  if self.publishing {
                      self.timer?.invalidate()
                      self.completion?(false)
                  }
                  else {
                      self.publishing = true
                      self.service.delegate = self
                      self.service.publish()
                      
                  }
              })
          }
          
          deinit {
              service.stop()
          }
      }
      
      extension LocalNetworkPrivacy : NetServiceDelegate {
          
          func netServiceDidPublish(_ sender: NetService) {
              timer?.invalidate()
              completion?(true)
          }
      }
      
      // How to use
      
      LocalNetworkPrivacy().checkAccessState { granted in
          print(granted)
      }
      

      ObjC

      您可以使用swift代码而无需重写ObjC,只需将swift文件添加到您的项目并直接调用checkAccessState(该函数必须标有@objc):

      #import "YourProjectName-Swift.h" // import swift classes to objc
      
      ...
      
      LocalNetworkPrivacy *local = [LocalNetworkPrivacy new];
      [local checkAccessStateWithCompletion:^(BOOL granted) {
          NSLog(@"Granted: %@", granted ? @"yes" : @"no");
      }];
      

      【讨论】:

      • 感谢分享。它适用于 TestFlight 构建吗?我正在广播一个虚拟 UDP 数据包以触发本地网络隐私权限警报,这适用于企业构建,但不适用于 Testflight 构建。
      • iUrii - 你检查了吗,上面的解决方案是否适用于 TestBuild?
      • @Ganeshpatro 是的,从我的测试中它可以很好地触发警报并使用 TestFlight 确定状态。
      • @iUrii 请提供相同的客观 c 代码。谢谢
      • @pradiprathod 你可以直接从objc使用swift类,看看我更新的答案。
      【解决方案6】:

      我写了这个类,如果你不在 iOS 14.2 上也可以使用。

      此类将提示用户访问本地网络的权限(第一次)。 如果已拒绝/授予,请验证现有权限状态。 请记住,这个实例必须保持活动状态,所以如果你在另一个类的函数调用中使用它,你需要在调用函数的范围之外保持实例活动。在某些情况下,您还需要网络多播权限。

      import UIKit
      import Network
      
      class LocalNetworkPermissionChecker {
          private var host: String
          private var port: UInt16
          private var checkPermissionStatus: DispatchWorkItem?
          
          private lazy var detectDeclineTimer: Timer? = Timer.scheduledTimer(
              withTimeInterval: .zero,
              repeats: false,
              block: { [weak self] _ in
                  guard let checkPermissionStatus = self?.checkPermissionStatus else { return }
                  DispatchQueue.main.asyncAfter(deadline: .now(), execute: checkPermissionStatus)
              })
          
          init(host: String, port: UInt16, granted: @escaping () -> Void, failure: @escaping (Error?) -> Void) {
              self.host = host
              self.port = port
              
              NotificationCenter.default.addObserver(
                  self,
                  selector: #selector(applicationIsInBackground),
                  name: UIApplication.willResignActiveNotification,
                  object: nil)
              
              NotificationCenter.default.addObserver(
                  self,
                  selector: #selector(applicationIsInForeground),
                  name: UIApplication.didBecomeActiveNotification,
                  object: nil)
              
              actionRequestNetworkPermissions(granted: granted, failure: failure)
          }
          
          deinit {
              NotificationCenter.default.removeObserver(self)
          }
          
          /// Creating a network connection prompts the user for permission to access the local network. We do not have the need to actually send anything over the connection.
          /// - Note: The user will only be prompted once for permission to access the local network. The first time they do this the app will be placed in the background while
          /// the user is being prompted. We check for this to occur. If it does we invalidate our timer and allow the user to make a selection. When the app returns to the foreground
          /// verify what they selected. If this is not the first time they are on this screen, the timer will not be invalidated and we will check the dispatchWorkItem block to see what
          /// their selection was previously.
          /// - Parameters:
          ///   - granted: Informs application that user has provided us with local network permission.
          ///   - failure: Something went awry.
          private func actionRequestNetworkPermissions(granted: @escaping () -> Void, failure: @escaping (Error?) -> Void) {
              guard let port = NWEndpoint.Port(rawValue: port) else { return }
              
              let connection = NWConnection(host: NWEndpoint.Host(host), port: port, using: .udp)
              connection.start(queue: .main)
              
              checkPermissionStatus = DispatchWorkItem(block: { [weak self] in
                  if connection.state == .ready {
                      self?.detectDeclineTimer?.invalidate()
                      granted()
                  } else {
                      failure(nil)
                  }
              })
              
              detectDeclineTimer?.fireDate = Date() + 1
          }
          
          /// Permission prompt will throw the application in to the background and invalidate the timer.
          @objc private func applicationIsInBackground() {
              detectDeclineTimer?.invalidate()
          }
          
          /// - Important: DispatchWorkItem must be called after 1sec otherwise we are calling before the user state is updated.
          @objc private func applicationIsInForeground() {
              guard let checkPermissionStatus = checkPermissionStatus else { return }
              DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: checkPermissionStatus)
          }
      }
      
      

      在函数范围之外声明以保持活动状态。如果整个调用类稍后没有被释放以取消订阅通知,请记住在完成后设置为 nil。

      可以这样使用:

      class RandomClass {
          var networkPermissionChecker: LocalNetworkPermissionChecker?
          
          func checkPermissions() {
              networkPermissionChecker = LocalNetworkPermissionChecker(host: "255.255.255.255", port: 4567, 
              granted: {
                  //Perform some action here...
              },
              failure: { error in
                  if let error = error {
                      print("Failed with error: \(error.localizedDescription)")
                  }
              })
          }
      }
      

      【讨论】:

      • 请提供相同的客观 c 代码。谢谢
      猜你喜欢
      • 2022-01-21
      • 2021-01-26
      • 2021-05-03
      • 2021-01-13
      • 2014-10-06
      • 2021-01-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多