Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Swift Reachability

Whether this device is online, answered by two signals that have to agree — as typed values, for a one-shot check or an app that stays up, on any Apple platform.

import Reachability

let verdict = await Reachability.check()
verdict.isOnline           // true
verdict.pathIsSatisfied    // true  — en0 has a route
verdict.probeSucceeded     // true  — and Apple's page answered, in 0.25 s
verdict.interfaces         // ["en0"]

let monitor = Reachability.Monitor()      // @MainActor, ObservableObject
monitor.start()
monitor.isOnline                          // flips within a second of Wi-Fi going off
monitor.offlineSince                      // and says since when

try await Reachability.publicIP()         // 125.26.175.94, v4

Why

Every app answers "am I online" in one of two wrong ways, and the difference only shows the day the network is actually broken:

The usual answer What goes wrong
NWPathMonitor says the path is satisfied It is satisfied while the router behind it is dead. A route to the router is a route; Wi-Fi stays "connected" through a power cut.
A HEAD through URLSession.shared Its cache can answer from disk. Last week's 200 comes back with the cable out, and the app reports online.
SCNetworkReachability Reports whether a packet could be routed, not whether one was. It has never proved the internet answered, and Apple has moved on from it.
One check at launch The one moment it cannot go wrong is the moment it was checked.

Two signals, both required: the path monitor for the instant answer when a route disappears, and a tiny request to a page that is always up, through a session that ignores every cache, for whether the route goes anywhere. Online means both said yes.

Installation

.package(url: "https://github.com/arraypress/swift-reachability.git", from: "0.1.0")

Foundation and Network. No dependencies, every Apple platform, macOS 14+.

One check

let verdict = await Reachability.check(timeout: 5, probe: .apple)

Reads the path first — by starting a monitor on a private queue, taking its first update and cancelling it, because currentPath before start is a placeholder that says every device is offline — and probes only when the path is up. A dead path means no probe was sent, and the verdict says so with probeSucceeded == nil rather than a false. The verdict carries the status the probe got, its latency, the interface names, and whether the route is metered or in Low Data Mode.

Two pages are built in, both checked with curl -I before they were chosen: Apple's captive-portal page (https://www.apple.com/library/test/success.html, answers 200) and Google's (https://www.google.com/generate_204, answers 204). Any status under 400 counts as reached — a redirect is still the internet answering. .custom(URL) takes a page of your own.

An app that stays up

@StateObject private var network = Reachability.Monitor()

network.start()                 // once, at launch
network.isOnline                // @Published
network.offlineSince            // @Published, nil while online
network.lastVerdict             // the evidence
network.noteOfflineError()      // a fetch just failed with a no-internet error: check now

The path monitor is the fast signal: a route going away flips isOnline at once. The probe is the confirmation: a route coming back does not flip it on until a request has reached the internet. While offline the probe repeats every ten seconds, so recovery shows within seconds; while online, once a minute, so a router that died between requests is noticed. The monitor starts optimistic, so nothing is drawn as offline before anything is known. Options sets the intervals, timeout and probe; override(online:) pins the state for previews and tests.

Which errors mean offline

URLError(.notConnectedToInternet).isOffline    // true
URLError(.timedOut).isOffline                  // false — a slow server is not a dead network
Reachability.classify(error)                   // the same test for any Error

The distinction a fetch needs before recording a failure. Not connected, connection lost, DNS failed, host not found or not reachable, roaming off, data not allowed and a call in progress are the network's fault. Backing off from a service for those punishes the wrong party.

The public address

try await Reachability.publicIP()               // whichever family the route uses
try await Reachability.publicIP(version: .v4)
try await Reachability.publicIP(version: .v6)   // upstream("no IPv6 route…") on most networks

From ipify, keyless. The body is validated as an address with inet_pton, so an error page is refused rather than stored. A network with no IPv6 route is not offline, and the .v6 failure says so instead.

Testing without a network

Transport, PathSource and Clock are protocols with the real implementations as defaults. The suite drives a scripted network, a scripted path and a clock moved by hand, so "Wi-Fi went off" and "sixty seconds passed" are lines in a test, and the monitor's every transition is pinned in milliseconds.

Tested

35 tests, none touching a network: the verdict for a dead path (no probe sent), a good probe, a redirect, 404 and 5xx, a timeout, a no-internet error, a path monitor that never answers, a hotspot flagged expensive, a custom probe; the monitor starting optimistic and confirming, dropping the instant the path goes, staying offline until a probe passes, catching a dead router on the minute, checking at once when a fetch fails, never doubling a probe, being pinned by override, stopping idempotently; every no-internet code and the ones that are not; the address lookup for each family, an error page, an empty body, a server error, a missing IPv6 route; and every model through JSON.

Licence

MIT.

About

Is this device online, answered honestly: a path check plus an uncached probe, one-shot or as a monitor, and the public IP — as typed values, on any Apple platform.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages