iOS 14 How to trigger Local Network dialog and check user answer?
Asked Answered
J

13

59

I've seen this Q/A What triggers, but it's not what I want. I also read this Network privacy permission check, but there is no answer. I also search for any methods or classes which can help me here: Network, but no luck again.

There is a new dialog for the Local Network authorization, where user can Allow/Don't Allow "to find and connect to devices on your local network".

enter image description here

But I'm struggling to find any API for how to trigger this popup and how to check is access granted or not(for example in AVCapture, I can check the authorization status for AVMediaType).

Thank you!

Justiciar answered 17/9, 2020 at 14:37 Comment(3)
There isn't a way to check YET. But looks like something is in the works: you can follow the developments here: developer.apple.com/forums/thread/650648Pier
Sadly @valosip's comment is correct. Also Idk why this question is getting downvoted since this is a massiv issue for some Apps.Hued
@Hued Yes, I don't understand either. But I already open DTS tech support, so I will update my answer after getting response from Apple.Justiciar
J
19

I did open DTS request and had conversion with Apple support team. Here is some important parts which I included below.

How to check is access granted or not

From support team:

For know, there is no such an API to check user permission.

From support team:

If the user declines, the connection fails. Exactly how it fails depends on the network API you’re using and how you use that API.

  • By default the connection will fail with NSURLErrorNotConnectedToInternet.
  • If you set waitsForConnectivity on the session configuration, the request will wait for things to improve. In that case you’ll receive the -URLSession:taskIsWaitingForConnectivity: delegate callback to tell you about this. If the user changes their mind and enables local network access, the connection will then go through.

Unfortunately there’s no direct way to determine if this behaviour is the result of a local network privacy restriction or some other networking failure.


How to trigger this popup

From support team:

the problem here is that the local network permission alert is triggered by outgoing traffic and you do not generate any outgoing traffic. The only way around this is to generate some dummy outgoing traffic in order to trigger this alert.

I’ve seen other developers in this situation and the absence of a direct API to trigger the local network permission alert is quite annoying. I encourage you to file a bug about this.

I’ve been discussing this issue with the local network privacy team and our current advice for apps in your situation — that is, apps that want to receive broadcasts but don’t send any local network traffic — is as follows:

  • The system should do a better job of handling this. We’re tracking that as a bug rdar://problem/67975514. This isn’t fixed in the current iOS 14.2b1 release but you should continue to test with iOS beta seeds as they are released.

  • In the meantime you can force the local network privacy alert to show by sending a message. We specifically recommend that you send a message that’s roughly equivalent to the message you’re trying to receive, so in your case that means sending an IPv4 UDP broadcast.

UPDATE

For iOS 14.2 - prompt is received for inbound traffic FIXED. Because of this you don't need below example for simulating traffic to triggering prompt.


Here is class for dummy outgoing traffic simulation: example

That traffic will never leave the iOS device and thus, even if the interface is asleep, it won’t wake it up. And even if it did wake up the interface, the cost of that is trivial because you’re not doing it over and over again, just once in order to trigger the local network privacy alert.

Justiciar answered 7/10, 2020 at 11:5 Comment(1)
See my answer for a way to get the popup using iOS APIs.Millwork
C
51

I found a way to trigger the prompt, receive a callback of the user's selection, and detect if the user has previously allowed or denied the prompt if it already appeared. To trigger the permission we use a service discovery API. When the user declines or previously declined we receive an error. It doesn't indicate if the permission was granted, so we also published a network service that returns success if the permission has been granted. By combining the 2 into a single component, we can trigger the prompt and get an indication of approval or decline: Until we receive success from the network service or error from the service discovery we assume that the permission is still pending.

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)
    }
}

How to use:

  1. Add LocalNetworkAuthorization class to your project
  2. Open .plist file and add "_bonjour._tcp", "_lnp._tcp.", as a values under "Bonjour services"
  3. Call requestAuthorization() to trigger the prompt or get the authorization status if it already been approved/denied
Cabinetmaker answered 30/5, 2021 at 5:49 Comment(9)
Thanks man the only clean answer I could find. I added async alternative and made it into a gist referencing you gist.github.com/doozMen/0b5fc54c765bccb7c13792caa4eaa51cElective
@Elective Very nice!Cabinetmaker
When the function runs on a background thread the netService is not schedule on any runloop and the netServiceDidPublish is never called. I had to schedule it like that to work: netService?.schedule(in: .main, forMode: .common)Tilburg
Where did you call it @TheoK, before or after the self.browser?.start(queue: .main)?Refugio
@JohnLima that's the code I used: netService = NetService(domain: "local.", type: "_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100) netService?.schedule(in: .main, forMode: .common) netService?.delegate = self self.browser?.start(queue: .main) netService?.publish() I can't format it better in a comment, each space is a line break.Tilburg
See my answer for a simpler way to get the popup using iOS APIs.Millwork
It works; great! Does it have to be _bonjour._tcp, because I already have _<my-service>._tcp for Multipeer Connectivity? If so, could you explain why these additional two .plist entries are needed? What is the result when there is no internet connection and when device is in airplane-mode?Millwork
This strategy works fine except for one case: If user first denies, then the app detects that, however if the user then grants while the app is running, my net service never comes up. Hence I can't detect the 'granted' case unless I request user to hard relaunch.Humanist
I'm getting a DNSServiceBrowse failed: NoAuth(-65555) on a device with iOS 16.3.1, and no alert comes up.Falsecard
J
19

I did open DTS request and had conversion with Apple support team. Here is some important parts which I included below.

How to check is access granted or not

From support team:

For know, there is no such an API to check user permission.

From support team:

If the user declines, the connection fails. Exactly how it fails depends on the network API you’re using and how you use that API.

  • By default the connection will fail with NSURLErrorNotConnectedToInternet.
  • If you set waitsForConnectivity on the session configuration, the request will wait for things to improve. In that case you’ll receive the -URLSession:taskIsWaitingForConnectivity: delegate callback to tell you about this. If the user changes their mind and enables local network access, the connection will then go through.

Unfortunately there’s no direct way to determine if this behaviour is the result of a local network privacy restriction or some other networking failure.


How to trigger this popup

From support team:

the problem here is that the local network permission alert is triggered by outgoing traffic and you do not generate any outgoing traffic. The only way around this is to generate some dummy outgoing traffic in order to trigger this alert.

I’ve seen other developers in this situation and the absence of a direct API to trigger the local network permission alert is quite annoying. I encourage you to file a bug about this.

I’ve been discussing this issue with the local network privacy team and our current advice for apps in your situation — that is, apps that want to receive broadcasts but don’t send any local network traffic — is as follows:

  • The system should do a better job of handling this. We’re tracking that as a bug rdar://problem/67975514. This isn’t fixed in the current iOS 14.2b1 release but you should continue to test with iOS beta seeds as they are released.

  • In the meantime you can force the local network privacy alert to show by sending a message. We specifically recommend that you send a message that’s roughly equivalent to the message you’re trying to receive, so in your case that means sending an IPv4 UDP broadcast.

UPDATE

For iOS 14.2 - prompt is received for inbound traffic FIXED. Because of this you don't need below example for simulating traffic to triggering prompt.


Here is class for dummy outgoing traffic simulation: example

That traffic will never leave the iOS device and thus, even if the interface is asleep, it won’t wake it up. And even if it did wake up the interface, the cost of that is trivial because you’re not doing it over and over again, just once in order to trigger the local network privacy alert.

Justiciar answered 7/10, 2020 at 11:5 Comment(1)
See my answer for a way to get the popup using iOS APIs.Millwork
P
11

In my case it was accessing this variable for some internal device statistics:

ProcessInfo.processInfo.hostName

Accessing this variable caused the alert to appear. If it doesn't cover your case perhaps you can search source code for some references around the local network/host.

Protoxide answered 7/10, 2020 at 10:25 Comment(4)
It's good but it doesn't have a callback unfortunately... But it does cause the popup to appear.Rinaldo
See my answer for another way to get the popup using iOS APIs.Millwork
I'm trying this now in iOS 17.3 and it is not making the permission popup appear.Zakaria
It's good and blocks the next lines of code until the user press any of the buttons. It solved my use case. thanksHibiscus
F
11

Because there is no API that directly returns your local network access state you can use next approach with publishing your Bonjour service and it returns the right result if access to local network was already set for your app (on app start e.g.). The approach causes the alert to appear as well but returns false before you select any button so to get the right result you should put this check to applicationDidBecomeActive and it will give the correct state after local network alert is disappeared and you return to your app.

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)")
    }
}

How to use:

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

NOTE: Don't forget to set NSLocalNetworkUsageDescription and add "_lnp._tcp." to NSBonjourServices in your Info.plist.

UPDATE

There is the second approach that works similar the code from above but can wait for an user's answer by checking an application state and then returns a valid access state for Local Network Privacy:

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

You can use swift code without rewriting to ObjC and to do that just add swift file to your project and call checkAccessState directly (the function must be marked with @objc):

#import "YourProjectName-Swift.h" // import swift classes to objc

...

LocalNetworkPrivacy *local = [LocalNetworkPrivacy new];
[local checkAccessStateWithCompletion:^(BOOL granted) {
    NSLog(@"Granted: %@", granted ? @"yes" : @"no");
}];
Frazer answered 19/1, 2021 at 22:7 Comment(6)
Thanks for sharing this. does it work on a TestFlight build? I am broadcasting a dummy UDP packet to trigger the Local Network privacy permission alert, this works for enterprise builds but does not work for Testflight builds.Peabody
iUrii - did you check, is this above soln working for TestBuild?Peabody
@Ganeshpatro Yes, it works fine from my tests with triggering the alert and determining a state with TestFlight.Frazer
@Frazer please provide objective c code for the same.ThanksTessellation
@pradiprathod You can use swift class directly from objc, look at my updated answer.Frazer
@Frazer any reason why you choose to use 2s for the time interval?Refugio
P
5

Apple has (late September 2020) published a Local Network Privacy FAQ which answers this, although it does seem that further changes to make this easier are likely.

There are Swift and Objective-C code examples for how to trigger the prompt by a workaround:

Currently there is no way to explicitly trigger the local network privacy alert (r. 69157424). However, you can bring it up implicitly by sending dummy traffic to a local network address. The code below shows one way to do this. It finds all IPv4 and IPv6 addresses associated with broadcast-capable network interfaces and sends a UDP datagram to each one. This should trigger the local network privacy alert, assuming the alert hasn’t already been displayed for your app.

And as for how to check result, keep your eye on this FAQ answer which says:

If your goal is to connect to a local network address using NWConnection then, starting with iOS 14.2 beta, you can use the unsatisfied reason property.

Podite answered 1/11, 2020 at 4:17 Comment(1)
using unsatisfied reason seems like the best way until an api will be introduce for checking result. Would love to get a working example. The best thing I found is - here and not fully helps mePileum
L
3

I wrote another class that can be used to trigger the prompt and find out whether Local Network permissions have been granted, without modifying any .plist files. It uses Swift and the Network framework.

import Network
class LocalNetworkPermissionTester {
    var connection: NWConnection
    var success = false
    var semaphore: DispatchSemaphore
    init(semaphore: DispatchSemaphore) {
        self.semaphore = semaphore
        let dispatchQueue = DispatchQueue(label: "LocalNetworkPermissionTester")
        self.connection = NWConnection(host: "127.255.255.255", port: 9, using: .udp)
        self.connection.stateUpdateHandler = { state in
            switch state {
            case .ready:
                self.success = true
                semaphore.signal()
            case .waiting(_):
                if case .localNetworkDenied? = self.connection.currentPath?.unsatisfiedReason {
                    self.success = false
                    semaphore.signal()
                }
            default:
                break
            }
        }
        connection.start(queue: dispatchQueue) // this will trigger the prompt if necessary
    }
}

It can be used like so:

let semaphore = DispatchSemaphore(value: 0)
let tester = LocalNetworkPermissionTester(semaphore: semaphore)
semaphore.wait()
if !tester.success {
    // if you just want to use this to trigger the prompt, you don't need this if statement
    // if you want to check whether the permission was granted previously, you can do that here and prompt the user to go to Settings > Privacy & Security > Local Network and grant the permission or whatever
}
Lengel answered 28/4, 2023 at 19:45 Comment(2)
Curious what is the reason for using semaphores here vs async/await? Is the goal to block the thread to avoid the app progressing, or is there some other importance?Catheryncatheter
@Catheryncatheter I don't totally remember but I believe it was to block the thread to avoid the app progressing.Lengel
D
1

Another workaround to consider if you're making a local network request with URLSession and would like the request to wait for the user to consent to the dialog is to set the waitsForConnectivity flag of URLSessionConfiguration to true:

Find:

URLSession.shared.dataTask(...)

Replace with:

// 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(...)

This will cause the request to hang until the dialog is either accepted or declined.

Directional answered 28/8, 2022 at 16:39 Comment(1)
Yes, this is mentioned in my answer, what is the difference?Justiciar
H
1

For reference, my solution to that problem is here. It is optimized for the use with SwiftUI where it can be used as an ObservableObject.

It bases on @TalSahar's approach, but also retries publishing the net service when it fails. This covers the case of the user granting access while the app is running:

browser.stateUpdateHandler = { [weak self] state in
    os_log("NWBrowser status update: %@", log: OSLog.default, type: .debug, "\(state)")
    guard let self else { return }
    switch state {
        case .failed(_):
            self.service?.publish()
        case .waiting(_):
            self.status = .denied
        default:
            break
    }
}
Humanist answered 20/2, 2023 at 16:45 Comment(0)
M
0

This works on (at least) iOS 16.

First create a MCNearbyServiceAdvertiser and MCNearbyServiceBrowser. Then, the popup appears when you start these 'services'; see start() in the code below.

Perhaps starting one of the too is sufficient too; I simply did both together, because that's what I needed.

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()
    }
}

See a bit more code here

See other answers here for how to check if the user granted this permission.

Millwork answered 2/2, 2023 at 18:6 Comment(0)
F
-1

I wrote up this class that can be used if you're not on iOS 14.2.

This class will prompt user for permission to access local network (first time). Verify existing permission state if already denied/granted. Just remember this instance has to be kept alive so if you are using this in a function call within another class you need to keep the instance alive outside of the scope of the calling function. You will also need the network multicasting entitlement under certain circumstances.

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)
    }
}

Declare outside the scope of the function in order to keep alive. Just remember to set to nil once you're done if the whole calling class isn't being deallocated later in order to unsubscribe to notifications.

Can be used like this:

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)")
            }
        })
    }
}
Flapdoodle answered 29/1, 2021 at 13:11 Comment(0)
R
-2

It can be triggered by sending dummy request with TCP IP socket. This code works perfectly for Flutter iOS app using Socket and IP address of the device itself:

    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..');
            }
Reprehend answered 21/6, 2022 at 6:29 Comment(0)
F
-2

ZeroConf/mDNS scans seem to trigger this permission request so anyone that is scanning for local broadcasts could just call the JS in their RN code when they want to trigger the permission request, e.g.:

import Zeroconf from 'react-native-zeroconf';

...

const zeroconf = new Zeroconf();

zeroconf.removeDeviceListeners();
zeroconf.addDeviceListeners();
zeroconf.scan('http', 'tcp', 'local.');
Frumpy answered 14/6 at 9:30 Comment(0)
P
-3

Based on @Roval's approach, I found that the hostName returned by ProcessInfo.processInfo.hostName always contains '.local' as suffix when the user granted the LAN access, perhaps this method can be used to determine whether the permission to access the local area network has been granted.

I also found that if you change the local network permission in the system settings, the application will not restart, so the returned hostName will not change. Therefore, this method has certain limitations. Here is my code:

func checkLanAccess(_ completed: Optional<(Bool) -> Void> = .none) {
    DispatchQueue.global(qos: .userInitiated).async {
        let hostName = ProcessInfo.processInfo.hostName
        let isGranted = hostName.contains(".local")
        if let completed {
            DispatchQueue.main.async {
                completed(isGranted)
            }
        }
    }
}
Placatory answered 21/3, 2023 at 2:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.