# Network Framework

Reach for Network.framework when URLSession is the wrong layer: raw TCP or
UDP, a WebSocket at the protocol level, Bonjour discovery, a local server, or
detailed path monitoring. Ordinary HTTP APIs stay on URLSession. Examples
assume Swift 6.3 and iOS 26.

## Contents

- [NWConnection](#nwconnection)
- [NWListener](#nwlistener)
- [NWBrowser](#nwbrowser)
- [NWPathMonitor](#nwpathmonitor)
- [NWParameters Configuration](#nwparameters-configuration)
- [TLS Configuration](#tls-configuration)
- [WebSocket Support](#websocket-support)
- [The iOS 26 NetworkConnection API](#the-ios-26-networkconnection-api)

## NWConnection

`NWConnection` is one bidirectional connection. It speaks TCP, UDP, TLS,
DTLS, QUIC and WebSocket depending on its parameters.
API reference: [NWConnection docs](https://developer.apple.com/documentation/network/nwconnection).

```swift
import Foundation
import Network
import OSLog

extension Logger { static let net = Logger(subsystem: "app.networking", category: "net") }

let link = NWConnection(host: "telemetry.example.net", port: 443, using: .tls)
link.stateUpdateHandler = { state in
    switch state {
    case .ready: Logger.net.info("connected")
    case .waiting(let error): Logger.net.notice("waiting for a path: \(error.localizedDescription)")
    case .failed(let error): Logger.net.error("failed: \(error.localizedDescription)"); link.cancel()
    case .cancelled: Logger.net.info("closed")
    default: break
    }
}
link.start(queue: .main)

link.send(content: Data("HELLO\n".utf8), contentContext: .defaultMessage, isComplete: true,
          completion: .contentProcessed { error in
              if let error { Logger.net.error("send failed: \(error.localizedDescription)") }
          })

func readLoop(on connection: NWConnection) {
    connection.receive(minimumIncompleteLength: 1, maximumLength: 65_536) { data, _, isComplete, error in
        if let data, !data.isEmpty { consume(data) }
        if let error {
            Logger.net.error("receive failed: \(error.localizedDescription)")
            return
        }
        if isComplete { connection.cancel(); return }
        readLoop(on: connection)
    }
}
```

UDP uses the same type:

```swift
let beacon = NWConnection(host: "239.255.10.20", port: 6200, using: .udp)
beacon.stateUpdateHandler = { state in
    if case .ready = state {
        beacon.send(content: Data("ping".utf8), completion: .contentProcessed { _ in })
    }
}
beacon.start(queue: .global())
```

- States run `setup`, `preparing`, `ready`, and end in `cancelled` or
  `failed`.
- `waiting(NWError)` means no usable path right now. The connection tries
  again by itself when the network comes back.
- Call `cancel()` when you are done. It frees the resources and moves the
  connection to `cancelled`.

## NWListener

`NWListener` accepts incoming connections, which is how you run a small TCP
or UDP server on the device.
API reference: [NWListener docs](https://developer.apple.com/documentation/network/nwlistener).

```swift
func startServer() throws -> NWListener {
    let port: NWEndpoint.Port = 8080
    let server = try NWListener(using: .tcp, on: port)
    server.stateUpdateHandler = { state in
        switch state {
        case .ready: Logger.net.info("listening on \(server.port?.rawValue ?? 0)")
        case .failed(let error): Logger.net.error("\(error.localizedDescription)"); server.cancel()
        default: break
        }
    }
    server.newConnectionHandler = { incoming in
        incoming.stateUpdateHandler = { state in
            if case .ready = state { readLoop(on: incoming) }
        }
        incoming.start(queue: .global())
    }
    server.service = NWListener.Service(name: "Workbench", type: "_workbench._tcp")
    server.serviceRegistrationUpdateHandler = { change in
        if case .add(let endpoint) = change { Logger.net.info("advertised \(String(describing: endpoint))") }
    }
    server.start(queue: .global())
    return server
}
```

`NWEndpoint.Port` is expressible by an integer literal, so a fixed port
needs no optional. For a port from user input, use
`NWEndpoint.Port(rawValue:)` and handle `nil`. Setting `service` advertises
the listener over Bonjour.

## NWBrowser

`NWBrowser` discovers peers that advertise over Bonjour (multicast DNS) on the LAN.
API reference: [NWBrowser docs](https://developer.apple.com/documentation/network/nwbrowser).

```swift
let scanner = NWBrowser(for: .bonjour(type: "_workbench._tcp", domain: nil), using: .tcp)
scanner.browseResultsChangedHandler = { results, changes in
    for change in changes {
        switch change {
        case .added(let found):
            if case .service(let name, _, _, _) = found.endpoint { Logger.net.info("found \(name)") }
        case .removed(let gone):
            Logger.net.info("lost \(String(describing: gone.endpoint))")
        default: break
        }
    }
}
scanner.start(queue: .main)

func connect(to found: NWBrowser.Result) -> NWConnection {
    NWConnection(to: found.endpoint, using: .tcp)
}
```

The handler receives the full current `results` set and the `changes`
(`.added`, `.removed`) since the last call.

## NWPathMonitor

`NWPathMonitor` replaces the deprecated `SCNetworkReachability`. It reports
whether a path exists and whether it is expensive or constrained.
API reference: [NWPathMonitor docs](https://developer.apple.com/documentation/network/nwpathmonitor).

```swift
@MainActor @Observable
final class LinkQuality {
    private(set) var online = true
    private(set) var metered = false
    private(set) var lowData = false

    func watch() async {
        for await path in NWPathMonitor() {
            online = path.status == .satisfied
            metered = path.isExpensive
            lowData = path.isConstrained
        }
    }

    var imagePolicy: ImagePolicy {
        if lowData { return ImagePolicy(quality: .low, prefetch: false, preloadVideo: false) }
        if metered { return ImagePolicy(quality: .standard, prefetch: true, preloadVideo: false) }
        return ImagePolicy(quality: .high, prefetch: true, preloadVideo: true)
    }
}
```

Start `watch()` from a `.task` so the loop ends with the view. To watch one
interface type, or on an SDK where the monitor is not yet an async sequence,
use the handler API:

```swift
let wifiOnly = NWPathMonitor(requiredInterfaceType: .wifi)
wifiOnly.pathUpdateHandler = { path in
    Logger.net.info("wifi path: \(String(describing: path.status))")
}
wifiOnly.start(queue: .global())
```

| `NWPath` member | Meaning |
|---|---|
| `status` | `.satisfied`, `.unsatisfied` or `.requiresConnection` |
| `isExpensive` | Cellular or a personal hotspot |
| `isConstrained` | Low Data Mode is on |
| `availableInterfaces` | The `NWInterface` values the path can use |
| `supportsDNS` | DNS resolution is available |
| `supportsIPv4`, `supportsIPv6` | Address families the path supports |

Adapt to the path: on a constrained path fetch low-quality images and skip
prefetching; on an expensive path keep standard quality and skip video
preloading; otherwise use high quality and prefetch freely.

## NWParameters Configuration

`NWParameters` describes the protocol stack and path rules for a connection
or listener. Build options objects directly and pass them in, which avoids
casting them back out of the stack:

```swift
let tcp = NWProtocolTCP.Options()
tcp.connectionTimeout = 10
tcp.enableKeepalive = true
tcp.keepaliveInterval = 30

let lanOnly = NWParameters(tls: nil, tcp: tcp)
lanOnly.requiredInterfaceType = .wifi
lanOnly.prohibitExpensivePaths = true
lanOnly.prohibitConstrainedPaths = true

let udp = NWProtocolUDP.Options()
udp.preferNoChecksum = true
let datagrams = NWParameters(dtls: nil, udp: udp)
```

If you must adjust a stack you did not build, use a conditional cast:
`if let tcp = params.defaultProtocolStack.transportProtocol as? NWProtocolTCP.Options`.

## TLS Configuration

TLS is configured with `NWProtocolTLS.Options`. ATS belongs to the URL
Loading System and does not police this layer, so the TLS version, cipher
choices and trust evaluation are yours to set. Deep trust work and SPKI
pinning belong in `swift-security`.

```swift
let tls = NWProtocolTLS.Options()
sec_protocol_options_set_min_tls_protocol_version(tls.securityProtocolOptions, .TLSv13)
let secure = NWParameters(tls: tls)

let session = NWConnection(host: "vault.example.net", port: 8443, using: secure)
session.stateUpdateHandler = { state in
    guard case .ready = state else { return }
    let raw = session.metadata(definition: NWProtocolTLS.definition)
    guard let meta = raw as? NWProtocolTLS.Metadata else { return }
    let version = sec_protocol_metadata_get_negotiated_tls_protocol_version(meta.securityProtocolMetadata)
    Logger.net.info("negotiated TLS \(version.rawValue)")
}
session.start(queue: .global())
```

## WebSocket Support

Network.framework exposes WebSocket through `NWProtocolWebSocket`, layered on
top of TLS and TCP.

```swift
let ws = NWProtocolWebSocket.Options()
ws.autoReplyPing = true
let params = NWParameters.tls
params.defaultProtocolStack.applicationProtocols.insert(ws, at: 0)
let socket = NWConnection(host: "stream.example.net", port: 443, using: params)

func sendText(_ message: String, over connection: NWConnection) {
    let meta = NWProtocolWebSocket.Metadata(opcode: .text)
    let context = NWConnection.ContentContext(identifier: "text", metadata: [meta])
    connection.send(content: Data(message.utf8), contentContext: context, isComplete: true,
                    completion: .contentProcessed { _ in })
}

func listen(on connection: NWConnection) {
    connection.receiveMessage { data, context, _, error in
        guard error == nil else { return }
        let meta = context?.protocolMetadata(definition: NWProtocolWebSocket.definition)
            as? NWProtocolWebSocket.Metadata
        switch meta?.opcode {
        case .text?, .binary?:
            if let data { consume(data) }
        case .close?:
            connection.cancel()
            return
        default:
            break
        }
        listen(on: connection)
    }
}
```

With `autoReplyPing` the framework answers pings for you. For app-level
WebSockets, `URLSessionWebSocketTask` (see
[background-websocket.md](background-websocket.md#urlsessionwebsockettask))
is usually simpler.

## The iOS 26 NetworkConnection API

`NetworkConnection` is the Swift-first connection type added in iOS 26. It is
generic over its protocol stack, uses async/await, and reports state through
closures. API reference: [NetworkConnection docs](https://developer.apple.com/documentation/network/networkconnection).

| | `NWConnection` | `NetworkConnection` |
|---|---|---|
| Availability | iOS 12+ | iOS 26+ |
| Stack typing | Untyped parameters | Generic `ApplicationProtocol` |
| Streams | One connection | Many QUIC streams share one connection |
| State | `stateUpdateHandler` | `onStateUpdate` and `onPathUpdate` closures |
| Wi-Fi Aware | Not available | `wifiAware` property |
| Style | Callbacks on a queue | Async calls, `Sendable`-aware closures |

```swift
let endpoint = NWEndpoint.hostPort(host: "relay.example.net", port: 9443)

let relay = NetworkConnection(to: endpoint) {
    TLS()
}

func logStates(of connection: NetworkConnection<TLS>) {
    connection.onStateUpdate { _, state in
        switch state {
        case .ready: Logger.net.info("ready")
        case .failed(let error): Logger.net.error("failed: \(error.localizedDescription)")
        case .cancelled: Logger.net.info("cancelled")
        default: break
        }
    }
}

logStates(of: relay)
try await relay.send(Data("status".utf8))
```

The stack can also be built separately and passed with
`NetworkConnection(to:using:)`. The connection establishes itself when it is
first used, so the first `send` or `receive` drives it to `ready`.

Keep HTTP on URLSession; `NetworkConnection` is for custom protocol stacks
and QUIC transport.

### QUIC Multiplexed Streams

A `NetworkConnection<QUIC>` carries many independent QUIC streams over one
connection. Opening and accepting streams are both `async throws`. Here `quic` is such a
connection:

```swift
let outbound = try await quic.openStream(directionality: .bidirectional)
try await outbound.send(Data("subscribe prices".utf8))

try await quic.inboundStreams { stream in
    let chunk = try await stream.receive(exactly: 64).content
    route(chunk)
}
```

`openStream` returns a `QUIC.Stream`; `inboundStreams` calls the closure for
every stream the peer opens. Treat both as fallible suspension points, never
as synchronous callbacks.

### Migration Guidance

- New low-level code that can require iOS 26: evaluate `NetworkConnection`
  first for typed stacks, first-class QUIC streams and `Sendable`-aware
  design.
- Apps that still support earlier releases keep `NWConnection`. It is not
  deprecated, and the two can live in the same app.

The snippets log through `extension Logger { static let net = Logger(subsystem: "app.networking", category: "net") }`.
