Using SCNetworkReachability in iOS: What It Actually Tells You
SCNetworkReachability tells you the network interface is up — not that the internet works. This guide shows the minimal Swift wrapper setup, how to interpret flags correctly, and the common mistakes that leak memory or mislead your UI.
10 Nov 2025, 03:09 UTC

The problem: reachable ≠ online
Apple's SCNetworkReachability API (often wrapped as Reachability) is the standard way to learn when a device's network interface changes state. The trap is treating a .reachable flag as proof that your API calls will succeed. It only means the interface is up — Wi‑Fi associated, cellular data enabled — not that packets reach the internet. Captive portals, corporate firewalls, and misconfigured DNS all return .reachable while blocking outbound traffic.
Takeaway: Use Reachability as a hint to retry or defer network work, not as a gate that guarantees connectivity. Always verify with an actual request.
How the API works
SCNetworkReachability is a C API in the SystemConfiguration framework. You create a reference for a target (hostname, IP, or NULL for the default route), set a callback, and schedule it on a run loop. The callback fires when flags change — interface up/down, WWAN vs Wi‑Fi, connection required, etc. Flags are a bitmask; you combine them with bitwise logic to decide what "connected" means for your app.
The popular Swift wrapper Reachability.swift (by Ashley Mills, maintained by the community) adds a delegate/notification layer and makes the flag parsing readable. It's the de facto standard for iOS projects since the Apple sample code was retired.
Worked example: minimal setup with the wrapper
Assume you've added Reachability.swift to your Xcode 15+ project targeting iOS 16+. The following runs on a background queue; UI updates must hop to main.
import SystemConfiguration
import Foundation
final class NetworkMonitor {
private let reachability: Reachability
private let queue = DispatchQueue(label: "com.example.reachability")
var onChange: ((Bool) -> Void)?
init() throws {
// nil = monitor default route (same as 0.0.0.0/::)
reachability = try Reachability()
}
func start() {
reachability.whenReachable = { [weak self] r in
let online = r.connection != .unavailable
DispatchQueue.main.async { self?.onChange?(online) }
}
reachability.whenUnreachable = { [weak self] _ in
DispatchQueue.main.async { self?.onChange?(false) }
}
do {
try reachability.startNotifier()
} catch {
print("Reachability start failed: \(error)")
}
}
func stop() {
reachability.stopNotifier()
}
}
// Usage
let monitor = try NetworkMonitor()
monitor.onChange = { isOnline in
print("Network \(isOnline ? "up" : "down")")
// Kick off sync, refresh UI, etc.
}
monitor.start()
// Call monitor.stop() when the owning object deinitializes
Key points in the example:
Reachability()with no argument watches the default route — the same interface the system uses for outbound traffic.startNotifier()registers the callback with the system run loop;stopNotifier()removes it. ForgettingstopNotifier()leaks the observer and keeps the instance alive.- The wrapper's
connectionproperty (enum.wifi,.cellular,.unavailable) abstracts the raw flags. - Callbacks arrive on a background queue; the example dispatches to main before touching UI.
Interpreting flags yourself (if you skip the wrapper)
If you call the C API directly, you'll see flags like:
| Flag | Meaning |
|---|---|
kSCNetworkReachabilityFlagsReachable | Interface is up and can send packets |
kSCNetworkReachabilityFlagsIsWWAN | Current path is cellular |
kSCNetworkReachabilityFlagsConnectionRequired | Traffic would trigger a dial/VPN |
kSCNetworkReachabilityFlagsConnectionOnDemand | Interface activates on demand |
kSCNetworkReachabilityFlagsInterventionRequired | User action needed (e.g., captive portal login) |
A common "online" heuristic:
let flags = reachability.currentReachabilityFlags
let reachable = flags.contains(.reachable)
let needsConnection = flags.contains(.connectionRequired)
let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic)
let isOnline = reachable && (!needsConnection || canConnectAutomatically)
Even this heuristic fails behind a captive portal — the interface is up, no connection required, but HTTP returns a login page.
Limits and common mistakes
1. Reachable does not mean internet
A hotel Wi‑Fi that redirects all HTTP to a splash page reports .reachable. Your URLSession request gets a 200 OK with HTML, not your JSON. Treat Reachability as "maybe try now" and handle HTTP errors, timeouts, and non-JSON responses.
2. Background suspension
When your app moves to background, iOS may suspend the run loop. The notifier stops firing until foreground. If you need connectivity awareness in background (e.g., for a BGAppRefreshTask), re-check flags at task start rather than relying on a stored state.
3. Thread delivery
Callbacks fire on the run loop you scheduled (usually a background queue). Any UI work — updating a banner, enabling a button — must dispatch to DispatchQueue.main. Crashes from UIKit access on background threads are a frequent bug report.
4. Memory leak from missing stopNotifier
The system retains your callback block. If the Reachability instance is deallocated without stopNotifier(), the observer stays registered. Run Instruments → Leaks after dismissing a view controller that owns a monitor; you'll see the Reachability instance and its block retained.
5. Simulator quirks
The simulator shares the Mac's network stack. Toggling Wi‑Fi on the Mac may not trigger a callback in the simulator. Test on a physical device or use Network Link Conditioner (Xcode → Open Developer Tool → Network Link Conditioner) to simulate loss, latency, and DNS failure.
Practical verification checklist
- Add
Reachability.swiftto a test project; run on device (iOS 16+). - Observe console logs while toggling Airplane Mode, Wi‑Fi, and cellular.
- Enable Network Link Conditioner → "100% Loss"; confirm callback fires and
connection == .unavailable. - Open Instruments → Leaks; navigate away from the screen that starts the monitor; verify zero leaks.
- Ship a test build to TestFlight; ask a tester on corporate Wi‑Fi with captive portal to confirm the app doesn't assume online just because
isOnline == true.
Version assumptions
Code above assumes Swift 5.9, Xcode 15, iOS 16 deployment target. The SystemConfiguration framework is stable; the wrapper's API has been consistent since ~2019. If you target iOS 15 or earlier, the same patterns apply but test on the oldest supported version.
Bottom line
SCNetworkReachability tells you the radio is on and an interface exists. It does not validate DNS, TLS, or application-layer reachability. Use it to avoid pointless request storms when the user is clearly offline, but always follow up with a real request and handle its failure modes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.