CoreWLAN on macOS: Long-Lived Wi-Fi Clients, Scans, and Event Recovery
Build CoreWLAN tools around one client, bounded scans, event ownership, error handling, and privacy-aware Wi-Fi diagnostics.
CoreWLAN is Apple’s framework for querying Wi-Fi interfaces and choosing wireless networks. It exposes the system’s WLAN subsystem through CWWiFiClient and interface objects such as CWInterface. The framework can support a diagnostics panel, an enterprise network utility, or a carefully scoped network-selection feature. It should not be mistaken for a generic packet-capture API or a replacement for Network.framework path monitoring.
Good CoreWLAN clients are long-lived, explicit about which interface they use, and careful about the cost of scans. A scan is not merely a local dictionary lookup: Apple’s API documents that a scan call blocks for its duration. Treat it as potentially slow work, report the distinction between a scan failure and an empty result, and do not put it on the main thread.
Own one Wi-Fi client for the feature lifetime
Apple notes that Wi-Fi client objects are resource intensive and recommends using a single long-running instance instead of repeatedly constructing short-lived clients. Obtain interface objects through the client rather than directly initializing them. The vended interface path is important for applications that use App Sandbox; Apple’s documentation says it enables sandboxed CoreWLAN use without special exceptions, while directly constructing interface objects can access lower-level system sockets that are denied by default in a sandbox.
Create the client in a service object whose lifetime matches the feature. If the feature is a one-shot command-line diagnostic, one client for the diagnostic process is sufficient. If it is a UI panel, keep the client while the panel or owning model is active, then release delegates and stop monitoring when the feature closes. Avoid a new client per refresh button press.
Do not hard-code en0 as if every Mac has exactly one Wi-Fi interface. Ask the client for its default interface and handle the absence of one. A Mac may have no Wi-Fi hardware, Wi-Fi may be disabled, the interface may be in a transient state, or a managed system may constrain access. These are distinct environment results; they are not all framework crashes.
Make scans bounded and asynchronous to the UI
scanForNetworks supports directed scans and broadcast scans. With a specific SSID, the interface performs a directed scan; with no SSID, it performs a broadcast scan. The call returns a set of CWNetwork values or throws an error in Swift. Since the method blocks while scanning, run it on a worker queue or a structured task that does not block AppKit’s main actor.
Avoid continuously scanning on a timer. A scan uses radio time, may affect battery life, and collects information about nearby networks. Prefer an explicit user action, a documented low-frequency refresh, or event-driven updates when the product needs them. Keep the result set bounded in the UI and discard stale snapshots when the user changes the selected interface or closes the panel.
Do not assume that every detected network is joinable. A CWNetwork result is an observation, not a promise about signal quality, security compatibility, authorization, or future reachability. The access point can disappear between scan and association. The interface can report a weak signal or a supported security type, but that does not prove the network is trusted. Enterprise credentials and certificate policy must be handled by the supported configuration path.
This small Swift example uses a client-vended default interface, runs the blocking scan from a command-line entry point, and converts framework errors into a process failure. An interactive application should move the scan off its UI thread and provide cancellation and progress state.
import CoreWLAN
import Foundation
func scanDefaultInterface() throws -> [String] {
let client = CWWiFiClient.shared()
guard let interface = client.interface() else {
throw NSError(
domain: "CoreWLAN",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "No default Wi-Fi interface is available."]
)
}
let networks = try interface.scanForNetworks(withSSID: nil)
return networks
.compactMap { $0.ssid }
.sorted()
}
do {
for ssid in try scanDefaultInterface() {
print(ssid)
}
} catch {
fputs("Wi-Fi scan failed: \(error.localizedDescription)\n", stderr)
exit(EXIT_FAILURE)
}
The example intentionally prints only SSIDs for a local operator. A production UI should decide whether network names may be logged or retained at all. SSIDs, BSSIDs, signal data, and scan timestamps can reveal a person’s location or routine. Do not attach them to analytics, crash reports, or support bundles by default.
Observe events without duplicating monitors
CWWiFiClient can deliver Wi-Fi events to an object conforming to CWEventDelegate. Start monitoring only the event types the feature uses. Retain the delegate for as long as monitoring remains active, and call the matching stop method when the owner is torn down. A lost delegate or duplicated observer can produce silent gaps or repeated UI updates.
Keep the event callback small. Capture immutable event facts, dispatch expensive work to a serial worker, and refresh the current interface state there. Event notifications describe changes; they are not a complete, durable event log. A process may start after a change or miss an update during a temporary client interruption. On startup and after clientConnectionInterrupted or clientConnectionInvalidated, re-read current state rather than attempting to reconstruct it from notifications alone.
Use a generation counter or request identifier when scans can overlap. If scan A starts, the user selects another interface, and scan B begins, a late result from A must not overwrite B’s state. Cancel or ignore stale work at the model layer. The framework’s synchronous scan may not be cancellable once underway, so keep a bounded number of in-flight requests and discard results whose generation no longer matches.
Separate Wi-Fi link state from IP reachability
CoreWLAN reports properties of Wi-Fi interfaces and networks. It does not prove that DNS works, a VPN is healthy, a captive portal has been completed, or a remote service is reachable. A connected Wi-Fi interface can have no usable route to a specific host. Conversely, an app may reach a service over Ethernet, VPN, or another path even if Wi-Fi is unavailable.
Use Network.framework for connection-level reachability and path changes, and use URLSession or an application protocol probe when the product needs to verify a service. Do not label a machine “offline” because CoreWLAN cannot find a Wi-Fi interface when another network path exists. Keep UI language specific: “Wi-Fi interface unavailable,” “not associated,” “scan failed,” and “service request failed” mean different things.
When a network selection feature asks the system to associate, do not store the user’s Wi-Fi password in app preferences or pass it on a command line. Prefer Apple’s supported configuration and keychain mechanisms, respect managed configuration, and do not automatically switch networks without a visible policy. Network names that look familiar are not cryptographic identities.
Handle permissions, policy, and runtime failures honestly
Access can vary with operating-system release, app sandboxing, device management, and privacy decisions. Use the current API contract for the supported macOS releases, check thrown errors, and test in the signed and sandboxed form that users receive. Do not assume that a successful development build has the same access as a production-signed app or a managed device.
The sample does not request location access or manipulate Wi-Fi state. If a specific feature requires a permission or entitlement, establish it from the current Apple documentation and include it only when needed. Never tell users to disable security controls as a generic troubleshooting step. A denied scan should become an actionable, accurate message and a diagnostic record that excludes credentials.
Handle common conditions explicitly: no Wi-Fi interface, powered-off radio, scan error, empty set, inaccessible network, stale result, and interruption. A zero-result scan is not necessarily a failure. A scan error should not be silently converted into an empty list, because the UI would mislead the user into thinking no networks are present.
Test lifecycle and data minimization
Test on hardware with Wi-Fi disabled, without a Wi-Fi interface where practical, with networks that appear and disappear during scans, and with a user who denies relevant privacy access. Exercise monitor start/stop repeatedly and verify callbacks cease after the feature closes. Test sleep/wake, network service changes, roaming between access points, and VPN connections so Wi-Fi events do not masquerade as end-to-end service failures.
For automated tests, wrap the framework behind a protocol and inject a fake client. Test empty scan results, thrown errors, delayed completions, duplicate events, stale generations, and teardown. This validates the product’s state machine without requiring the test runner to reconfigure the machine’s actual radio.
Collect the least information needed. If a diagnostic requires SSID, BSSID, channel, or signal metrics, explain why, restrict retention, and redact values from general logs. Keep credentials and key material out of output entirely. A support bundle should be opt-in and previewable before upload.
Operational checklist
Before release, confirm that one long-lived client is used per feature owner, interfaces come from that client, scans do not block the UI, monitors are stopped symmetrically, stale results are ignored, and runtime errors remain distinct from empty results. Validate the signed sandboxed build on supported macOS releases and use a separate path for IP-level reachability.
CoreWLAN is useful when the product genuinely needs Wi-Fi-specific observations. Its APIs are not a substitute for a connection model, and access to nearby-network data deserves privacy care. A reliable implementation is intentionally quiet: it observes only when needed, makes no unsupported claims, and leaves network selection under an explicit user or administrator policy.
Related:
- Fixing Wi-Fi That Keeps Dropping After a macOS Update
- NWConnection on macOS: Stream Framing, State, and Cancellation
Sources: