swift iOS 14如何触发本地网络对话框并检查用户答案?

gz5pxeao  于 2023-02-03  发布在  Swift
关注(0)|答案(9)|浏览(553)

我看过这个What triggers的问答,但不是我想要的。我也看过这个Network privacy permission check,但没有答案。我也在这里搜索任何可以帮助我的方法或类:Network,但又没有运气。
有一个用于本地网络授权的新对话框,用户可以在其中允许/不允许"查找并连接到本地网络"上的设备。

但是我很难找到任何API来触发这个弹出窗口,以及如何检查访问是否被授权(例如,在AVCapture中,我可以检查AVMediaType的授权状态)。
谢谢大家!

q9yhzks0

q9yhzks01#

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

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类添加到项目中
1.打开. plist文件并添加"_bonjour._tcp"、"_lnp._tcp."作为"Bonjour服务"下的值
1.调用requestAuthorization()触发提示,或者获取授权状态(如果已批准/拒绝)

aydmsdu9

aydmsdu92#

我确实打开了DTS请求,并与苹果支持团队进行了转换。这里是我包括在下面的一些重要部分。

如何检查是否允许访问

  • 来自支持团队:*

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

  • 来自支持团队:*

如果用户拒绝,连接将失败。失败的确切方式取决于您使用的网络API以及您使用该API的方式。

  • 默认情况下,连接将失败,并显示NSURLErrorNotConnectedToInternet
  • 如果你在会话配置中设置了waitsForConnectivity,请求将等待情况改善,这时你将收到-URLSession:taskIsWaitingForConnectivity: delegate回调来告诉你这一点,如果用户改变主意并启用了本地网络访问,连接将继续进行。

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

如何触发此弹出窗口

  • 来自支持团队:*

这里的问题是本地网络权限警报是由传出通信量触发的,而您没有生成任何传出通信量。2唯一的解决办法是生成一些假传出通信量来触发此警报。
我见过其他开发人员遇到过这种情况,缺乏直接的API来触发本地网络权限警报是相当烦人的。我鼓励你提交一个关于这个问题的bug。
我一直在与本地网络隐私团队讨论这个问题,我们目前对您所处情况下的应用(即希望接收广播但不发送任何本地网络流量的应用)的建议如下:

  • 系统应该能更好地处理这个问题。我们将其作为一个bug rdar://problem/67975514进行跟踪。当前的iOS 14.2b1版本没有修复这个问题,但你应该在iOS beta种子发布时继续进行测试。
  • 同时,您可以通过发送消息强制显示本地网络隐私警报。我们特别建议您发送与您尝试接收的消息大致相同的消息,因此,在您的情况下,这意味着发送IPv4 UDP广播。

更新

对于iOS 14.2-入站流量收到提示已修复。因此,您不需要下面的示例来模拟触发提示的流量。
以下是虚拟传出流量模拟的类:example
这些流量永远不会离开iOS设备,因此,即使界面处于休眠状态,也不会唤醒它;即使它唤醒了界面,其成本也微不足道,因为你不会一遍又一遍地这么做,只需要一次就可以触发本地网络隐私警报。

2skhul33

2skhul333#

在我的例子中,它访问这个变量来获取一些内部设备统计信息:

ProcessInfo.processInfo.hostName

访问此变量导致出现警报。如果它不适用于您的情况,也许您可以搜索源代码以查找本地网络/主机周围的一些引用。

gijlo24d

gijlo24d4#

由于没有直接返回本地网络访问状态的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并在Info.plist中将"_lnp._tcp."添加到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)
}
    • 目标C**

您可以使用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");
}];
fkaflof6

fkaflof65#

苹果已经(2020年9月下旬)发布了Local Network Privacy FAQ来回答这个问题,尽管看起来有可能做出进一步的改变来使之更容易。
SwiftObjective-C代码示例说明了如何通过变通方法触发提示:
目前还没有明确触发本地网络隐私警报的方法(第69157424条规则)。然而,您可以通过向本地网络地址发送虚拟流量来隐式启动它。以下代码显示了一种实现此操作的方法。它查找与支持广播的网络接口关联的所有IPv4和IPv6地址,并向每个地址发送UDP数据报。这应该会触发本地网络隐私警报,假设尚未为您的应用显示提醒。
至于如何检查结果,请关注this FAQ answer,其中显示:
如果您的目标是使用NWConnection连接到本地网络地址,则从iOS 14.2 beta开始,您可以使用unsatisfied reason属性。

lmvvr0a8

lmvvr0a86#

它可以通过使用TCP IP套接字发送虚拟请求来触发。此代码使用套接字和设备本身的IP地址完美地适用于Flutter iOS应用程序:

import 'package:network_info_plus/network_info_plus.dart';
        import 'dart:io';
try{
        
        var deviceIp = await NetworkInfo().getWifiIP();
             
        
              Duration? timeOutDuration = Duration(milliseconds: 100);
              await Socket.connect(deviceIp, 80, timeout: timeOutDuration);
            } catch (e) {
              print(
                  'Exception..');
            }
hmae6n7t

hmae6n7t7#

如果您使用URLSession发出本地网络请求,并希望请求等待用户同意对话框,则需要考虑的另一种解决方法是将URLSessionConfigurationwaitsForConnectivity标志设置为true
查找:

URLSession.shared.dataTask(...)

替换为:

// Default config
let config = URLSessionConfiguration.default
        
// Wait for user to consent to local network access
if #available(iOS 11.0, *) {
    config.waitsForConnectivity = true
}

// Execute network request
let task = URLSession(configuration: config).dataTask(...)

这将导致请求挂起,直到对话框被接受或拒绝。

fzwojiic

fzwojiic8#

这在(至少)iOS 16上有效。
首先创建一个MCNearbyServiceAdvertiserMCNearbyServiceBrowser,然后在启动这些“服务”时弹出窗口;请参见下面代码中的start()
也许开始一个太也就足够了;我只是把两者都做了,因为这正是我所需要的。

class Connector : NSObject, ObservableObject
{
    @Published var peers = [MCPeerID]()
    @Published var event: String?

    private let serviceType = "app"
    private let peerId = MCPeerID(displayName: UIDevice.current.name)
    private let serviceAdvertiser: MCNearbyServiceAdvertiser
    private let serviceBrowser: MCNearbyServiceBrowser
    private let session: MCSession

    private let log = Logger()

    override init()
    {
        session = MCSession(peer: peerId, securityIdentity: nil, encryptionPreference: .none)
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerId,
                                                      discoveryInfo: ["event" : "hello"],
                                                      serviceType: serviceType)
        serviceBrowser = MCNearbyServiceBrowser(peer: peerId, serviceType: serviceType)

        super.init()

        session.delegate = self
        serviceAdvertiser.delegate = self
        serviceBrowser.delegate = self
    }

    deinit
    {
        serviceAdvertiser.stopAdvertisingPeer()
        serviceBrowser.stopBrowsingForPeers()
    }

    func start()
    {
        serviceAdvertiser.startAdvertisingPeer()
        serviceBrowser.startBrowsingForPeers()
    }
}

查看更多代码here
有关如何检查用户是否授予了此权限,请参阅此处的其他答案。

z5btuh9x

z5btuh9x9#

我写了这个类,如果你没有使用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)")
            }
        })
    }
}

相关问题