CoreWLAN scanForNetworks does not expose SSIDs even when Location permission is authorized (Sequoia 15.1)

Hi, I've read a bunch of threads regarding the changes in Sonoma and later requiring Location permission for receiving SSIDs. However, as far as I can see, in Sequoia 15.1 SSIDs and BSSIDs are empty regardless.

In particular, this makes it not possible to use associate(withName:) and associate(withSSID:) because the network object returned by scanForNetwork(withSSID: "...") has its .ssid and .bssid set to nil.

Here is an example:

  1. First we have a wrapper to call the code after the location permission is authorized:
import Foundation
import CoreLocation

class LocationDelegate: NSObject, CLLocationManagerDelegate {
    var onAuthorized: (() -> Void)?
    var onDenied: (() -> Void)?

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        let authStatus = manager.authorizationStatus
        print("Location authorization status changed: \(authStatusToString(authStatus))")

        if authStatus == .authorizedAlways {
            onAuthorized?()
        } else if authStatus == .denied || authStatus == .restricted {
            onDenied?()
        }
    }
}

let locationManager = CLLocationManager()
let locationDelegate = LocationDelegate()

func authorizeLocation(onAuthorized: @escaping () -> Void, onDenied: @escaping () -> Void) {
    let authStatus = locationManager.authorizationStatus
    print("Location authorization status: \(authStatusToString(authStatus))")

    if authStatus == .notDetermined {
        print("Waiting for location authorization...")

        locationDelegate.onAuthorized = onAuthorized
        locationDelegate.onDenied = onDenied
        locationManager.delegate = locationDelegate
        locationManager.requestAlwaysAuthorization()

    } else if authStatus == .authorizedAlways {
        onAuthorized()

    } else if authStatus == .denied || authStatus == .restricted {
        onDenied()
    }

    RunLoop.main.run()
}

func authStatusToString(_ status: CLAuthorizationStatus) -> String {
    switch status {
    case .notDetermined:
        return "Not Determined"
    case .restricted:
        return "Restricted"
    case .denied:
        return "Denied"
    case .authorizedAlways:
        return "Always Authorized"
    case .authorizedWhenInUse:
        return "Authorized When In Use"
    @unknown default:
        return "Unknown"
    }
}
  1. Then, a demo program itself:
import Foundation
import CoreWLAN
import Network

let client = CWWiFiClient.shared()

guard let interface = client.interface() else {
    print("No wifi interface")
    exit(1)
}

authorizeLocation(
    onAuthorized: {
        do {
            print("Scanning for wifi networks...")
            let scanResults = try interface.scanForNetworks(withSSID: nil)
            let networks = scanResults.compactMap { network -> [String: Any]? in
                return [
                    "ssid": network.ssid ?? "unknown",
                    "bssid": network.bssid ?? "unknown"
                ]
            }
            let jsonData = try JSONSerialization.data(withJSONObject: networks, options: .prettyPrinted)
            if let jsonString = String(data: jsonData, encoding: .utf8) {
                print(jsonString)
            }
            exit(0)
        } catch {
            print("Error: \(error)")
            exit(1)
        }
    },

    onDenied: {
        print("Location access denied")
        exit(1)
    }
)

When launched, the program asks for permission, and after that, is shown as enabled in Privacy & Security Settings panel.

Here is the output where it can be seen that the scan is performed after location access was authorized, and regardless of that, all ssids are empty:

Location authorization status: Not Determined
Waiting for location authorization...
Location authorization status changed: Always Authorized
Scanning for wifi networks...
[
  {
    "ssid" : "unknown",
    "bssid" : "unknown"
  },
  {
    "ssid" : "unknown",
    "bssid" : "unknown"
  },
.... further omitted

Calling scanForNetworks() with explicitly specified network name does this as well, returns a CWNetwork object with .ssid / .bssid = nil.

However, as far as I can see, in Sequoia 15.1 SSIDs and BSSIDs are empty regardless.

Was this code previously working on 15.0?

Then, a demo program itself:

That looks like it was built as a command-line tool. If you put the same code into an app — starting from Xcode’s built-in macOS > App template — does it work there?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi Quinn,

Thank you for suggestions. I've tried on my other machine, so I can confirm that:

  • It does work as expected on 15.0.1 (Intel).
  • But does not work on 15.1.1 (ARM).

Actually it's currently built as a bundle and I'm testing it that way. But I tried both ways, a bundle and a standalone executable. Also tried with a provisioning profile (not sure if that matters).

Also tried with a provisioning profile (not sure if that matters).

Provisioning profiles aren’t a concern here. On macOS their function is to authorise restricted entitlements, and there are no restricted entitlements in the Core WLAN space.

But I tried both ways, a bundle and a standalone executable.

I want to make sure we’re on the same page about the test I asked you to do. I’m suggesting that you create a proper GUI app with a button (or menu item, or whatever) that’s wired up to your test code. So, you run your app from Xcode and then, to run your test, you click the button.

Is that what you’re doing?

I’m concerned that you’re dropping your command-line tool code into the app’s function [1], which isn’t a great test.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] À la Signing a daemon with a restricted entitlement.

Thanks Quinn,

You're right, it's a CLI tool wrapped in an app bundle however, it's not running as a daemon and it is able get the location authorization and to print my location.

So, following your advice, I've wrapped this into a GUI app, and surprise, it did work correctly.

So now I don't really get what makes the difference.

They both:

  • include exactly the same entitlements,
  • are able to receive the location permission and print my location,
  • neither of them has those NSLocation* strings in Info.plist.

But the GUI app is able to get the list of networks, and the CLI app is not.

Can it be a matter of having an AppDelegate?

I've wrapped this into a GUI app, and surprise, it did work correctly.

Oh, interesting. Honestly, I was expect that to go the other way, that is, the problem would replicate even when you ran your code in an app context.

I’m quite mystified as to why these behave differently. You wrote:

Can it be a matter of having an AppDelegate?

Possibly, but I’m struggling to think of why that would affect this.

My best guess is that this is something to do with run loops. The code you posted earlier runs the run loop only while waiting for location authorisation, whereas a normal app runs the run loop continuously.

In your code you’re calling scanForNetworks(…) in your onAuthorized closure, which means it’s running on the main thread. Did you end up doing the same in your app version?

Actually, is there any chance you can upload the failing command-line tool test project and the working app test project to somewhere I can access them, like GitHub? I’d like to take a more detailed look at the code.

ps We have general advice on this sort of thing in Creating a test project.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

CoreWLAN scanForNetworks does not expose SSIDs even when Location permission is authorized (Sequoia 15.1)
 
 
Q