| name | axiom-network-framework-ref |
| description | Reference — Comprehensive Network.framework guide covering NetworkConnection (iOS 26+), NWConnection (iOS 12-25), TLV framing, Coder protocol, NetworkListener, NetworkBrowser, Wi-Fi Aware discovery, and migration strategies |
| license | MIT |
| compatibility | iOS 12+ (NWConnection), iOS 26+ (NetworkConnection) |
| metadata | {"version":"1.0.0","last-updated":"2025-12-02"} |
Network.framework API Reference
Overview
Network.framework is Apple's modern networking API that replaces Berkeley sockets, providing smart connection establishment, user-space networking, built-in TLS support, and seamless mobility. Introduced in iOS 12 (2018) with NWConnection and evolved in iOS 26 (2025) with NetworkConnection for structured concurrency.
Evolution timeline
- 2018 (iOS 12) NWConnection with completion handlers, deprecates CFSocket/NSStream/SCNetworkReachability
- 2019 (iOS 13) User-space networking (30% CPU reduction), TLS 1.3 default
- 2025 (iOS 26) NetworkConnection with async/await, TLV framing built-in, Coder protocol, Wi-Fi Aware discovery
Key capabilities
- Smart connection establishment Happy Eyeballs (IPv4/IPv6 racing), proxy evaluation (PAC), VPN detection, WiFi Assist fallback
- User-space networking ~30% lower CPU usage vs sockets, memory-mapped regions, reduced context switches
- Built-in security TLS 1.3 by default, DTLS for UDP, certificate pinning support
- Mobility Automatic network transition handling (WiFi ↔ cellular), viability notifications, Multipath TCP
- Performance ECN (Explicit Congestion Notification), service class marking, TCP Fast Open, UDP batching
When to use vs URLSession
- URLSession HTTP, HTTPS, WebSocket, simple TCP/TLS streams → Use URLSession (optimized for these)
- Network.framework UDP, custom protocols, low-level control, peer-to-peer, gaming, streaming → Use Network.framework
Related Skills
- Use
axiom-networking for anti-patterns, common patterns, pressure scenarios
- Use
axiom-networking-diag for systematic troubleshooting of connection failures
When to Use This Skill
Use this skill when:
- Planning migration from BSD sockets, CFSocket, NSStream, or SCNetworkReachability
- Understanding API differences between NWConnection (iOS 12-25) and NetworkConnection (iOS 26+)
- Implementing all 12 WWDC 2025 examples (TLS connection, TLV framing, Coder protocol, NetworkListener, Wi-Fi Aware)
- Choosing protocols (TCP, UDP, TLS, QUIC) for your use case
- Peer-to-peer discovery setup with NetworkBrowser and Wi-Fi Aware
- Optimizing performance with user-space networking, batching, pacing
- Migrating from completion handlers to async/await (NWConnection → NetworkConnection)
API Evolution
Timeline
| Year | iOS Version | Key Features |
|---|
| 2018 | iOS 12 | NWConnection, NWListener, NWBrowser introduced |
| 2019 | iOS 13 | User-space networking (30% CPU reduction), TLS 1.3 default |
| 2021 | iOS 15 | WebSocket support in URLSession |
| 2025 | iOS 26 | NetworkConnection (async/await), TLV framing, Coder protocol, Wi-Fi Aware |
NWConnection (iOS 12-25) vs NetworkConnection (iOS 26+)
| Feature | NWConnection (iOS 12-25) | NetworkConnection (iOS 26+) |
|---|
| Async model | Completion handlers | async/await structured concurrency |
| State updates | stateUpdateHandler callback | states AsyncSequence |
| Send | send(content:completion:) callback | try await send(content) suspending |
| Receive | receive(minimumIncompleteLength:maximumLength:completion:) | try await receive(exactly:) suspending |
| Framing | Manual or custom NWFramer | TLV built-in (TLV { TLS() }) |
| Codable | Manual JSON encode/decode | Coder protocol (Coder(MyType.self, using: .json)) |
| Memory | Requires [weak self] in all closures | No [weak self] needed (Task cancellation automatic) |
| Error handling | Check error in completion | throws with natural propagation |
| State machine | Callbacks on state changes | for await state in connection.states |
| Discovery | NWBrowser (Bonjour only) | NetworkBrowser (Bonjour + Wi-Fi Aware) |
Recommendation
- New apps targeting iOS 26+: Use NetworkConnection (cleaner, safer)
- Apps supporting iOS 12-25: Use NWConnection (backward compatible)
- Migration: Both APIs coexist, migrate incrementally
NetworkConnection (iOS 26+) Complete Reference
4.1 Creating Connections
NetworkConnection uses declarative protocol stack composition.
Example 1: Basic TLS Connection (WWDC 4:04)
import Network
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
TLS()
}
public func sendAndReceiveWithTLS() async throws {
let outgoingData = Data("Hello, world!".utf8)
try await connection.send(outgoingData)
let incomingData = try await connection.receive(exactly: 98).content
print("Received data: \(incomingData)")
}
Key points
TLS() infers TCP() and IP() automatically
- No explicit connection.start() needed (happens on first send/receive)
- Async/await eliminates callback nesting
Example 2: Custom IP Options (WWDC 4:41)
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
TLS {
TCP {
IP()
.fragmentationEnabled(false)
}
}
}
When to customize IP
.fragmentationEnabled(false) — For protocols that handle fragmentation themselves (QUIC)
.ipVersion(.v6) — Force IPv6 only (testing)
Example 3: Custom Parameters (WWDC 5:07)
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029),
using: .parameters {
TLS {
TCP {
IP()
.fragmentationEnabled(false)
}
}
}
.constrainedPathsProhibited(true)
)
Common parameters
.constrainedPathsProhibited(true) — Respect low data mode
.expensivePathsProhibited(true) — Don't use cellular/hotspot
.multipathServiceType(.handover) — Enable Multipath TCP
Endpoint Types
.hostPort(host: "example.com", port: 443)
.service(name: "MyPrinter", type: "_ipp._tcp", domain: "local.", interface: nil)
.unix(path: "/tmp/my.sock")
Protocol Stack Composition
TLS()
QUIC()
UDP()
TCP()
WebSocket {
TLS()
}
TLV {
TLS()
}
4.2 State Machine
NetworkConnection transitions through these states:
setup
↓
preparing (DNS, TCP handshake, TLS handshake)
↓
┌─ waiting (no network, retrying)
│ ↓
└→ ready (can send/receive)
↓
failed (error) or cancelled
Monitoring States
Task {
for await state in connection.states {
switch state {
case .preparing:
print("Connecting...")
case .waiting(let error):
print("Waiting for network: \(error)")
case .ready:
print("Connected!")
case .failed(let error):
print("Failed: \(error)")
case .cancelled:
print("Cancelled")
@unknown default:
break
}
}
}
Key states
- .preparing DNS lookup, TCP SYN, TLS handshake
- .waiting No network available, framework retries automatically
- .ready Connection established, can send/receive
- .failed Unrecoverable error (server refused, TLS failed, timeout)
- .cancelled Task cancelled or connection.cancel() called
4.3 Send/Receive Patterns
Send: Basic
let data = Data("Hello".utf8)
try await connection.send(data)
Receive: Exact Byte Count (WWDC 7:30)
let incomingData = try await connection.receive(exactly: 98).content
print("Received \(incomingData.count) bytes")
Receive: Variable Length (WWDC 8:29)
let remaining32 = try await connection.receive(as: UInt32.self).content
guard var remaining = Int(exactly: remaining32) else { throw MyError.invalidLength }
while remaining > 0 {
let chunk = try await connection.receive(atLeast: 1, atMost: remaining).content
remaining -= chunk.count
}
receive() variants
receive(exactly: n) — Wait for exactly n bytes
receive(atLeast: min, atMost: max) — Get between min and max bytes
receive(as: UInt32.self) — Read fixed-size type (network byte order)
4.4 TLV Framing (iOS 26+)
TLV (Type-Length-Value) solves message boundary problem on stream protocols (TCP/TLS).
Format
- Type: UInt32 (message identifier)
- Length: UInt32 (message size, automatic)
- Value: Message bytes
Example: GameMessage with TLV (WWDC 11:06, 11:24, 11:53)
import Network
enum GameMessage: Int {
case selectedCharacter = 0
case move = 1
}
struct GameCharacter: Codable {
let character: String
}
struct GameMove: Codable {
let row: Int
let column: Int
}
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
TLV {
TLS()
}
}
public func sendWithTLV() async throws {
let characterData = try JSONEncoder().encode(GameCharacter(character: "🐨"))
try await connection.send(characterData, type: GameMessage.selectedCharacter.rawValue)
}
public func receiveWithTLV() {
(incomingData, metadata) connection.receive()
(rawValue: metadata.type) {
.selectedCharacter:
character ().decode(., from: incomingData)
()
.move:
move ().decode(., from: incomingData)
()
.none:
()
}
}
Benefits
- Message boundaries preserved (send 3 messages → receive exactly 3)
- Type-safe message handling (enum-based routing)
- Minimal overhead (8 bytes per message: type + length)
When to use
- Mixed message types (chat + presence + typing)
- Existing protocols using TLV
- Need message boundaries without heavy framing
4.5 Coder Protocol (iOS 26+)
Coder eliminates manual JSON encoding/decoding boilerplate.
Example: GameMessage with Coder (WWDC 12:50, 13:13, 13:53)
import Network
enum GameMessage: Codable {
case selectedCharacter(String)
case move(row: Int, column: Int)
}
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
Coder(GameMessage.self, using: .json) {
TLS()
}
}
public func sendWithCoder() async throws {
let selectedCharacter: GameMessage = .selectedCharacter("🐨")
try await connection.send(selectedCharacter)
}
public func receiveWithCoder() async throws {
let gameMessage = try await connection.receive().content
switch gameMessage {
case .selectedCharacter(let character):
print("Character selected: ")
.move( row, column):
()
}
}
Supported formats
.json — JSON encoding (human-readable, widely compatible)
.propertyList — Property list (faster, smaller)
Benefits
- No JSON boilerplate (~50 lines → ~10 lines)
- Type-safe (compiler catches message structure changes)
- Automatic framing (handles message boundaries)
When to use
- App-to-app communication (you control both ends)
- Prototyping (fastest time to working code)
- Type-safe protocols
When NOT to use
- Interoperating with non-Swift servers
- Need custom wire format
- Performance-critical (prefer manual encoding for control)
4.6 NetworkListener (iOS 26+)
Listen for incoming connections with automatic subtask management.
Example: Listening for Connections (WWDC 15:16)
import Network
public func listenForIncomingConnections() async throws {
try await NetworkListener {
Coder(GameMessage.self, using: .json) {
TLS()
}
}.run { connection in
for try await (gameMessage, _) in connection.messages {
switch gameMessage {
case .selectedCharacter(let character):
print("Player chose: \(character)")
case .move(let row, let column):
print("Player moved: (\(row), \(column))")
}
}
}
}
Key features
- Automatic subtask per connection (no manual Task management)
- Structured concurrency (all subtasks cancelled when listener exits)
connection.messages async sequence for receiving
Listener configuration
NetworkListener(port: 1029) { TLS() }
NetworkListener { TLS() }
NetworkListener(service: .init(name: "MyApp", type: "_myapp._tcp")) { TLS() }
4.7 NetworkBrowser & Wi-Fi Aware (iOS 26+)
Discover endpoints on local network or nearby devices.
Example: Wi-Fi Aware Discovery (WWDC 17:39)
import Network
import WiFiAware
public func findNearbyDevice() async throws {
let endpoint = try await NetworkBrowser(
for: .wifiAware(.connecting(to: .allPairedDevices, from: .ticTacToeService))
).run { endpoints in
.finish(endpoints.first!)
}
let connection = NetworkConnection(to: endpoint) {
Coder(GameMessage.self, using: .json) {
TLS()
}
}
}
Wi-Fi Aware features
- Peer-to-peer without infrastructure (no WiFi router needed)
- Automatic discovery of paired devices
- Low latency, axiom-high throughput
- iOS 26+ only
Browse descriptors
.bonjour(type: "_http._tcp", domain: "local")
.wifiAware(.connecting(to: .allPairedDevices, from: .myService))
.wifiAware(.connecting(to: .pairedDevice(identifier: deviceID), from: .myService))
NWConnection (iOS 12-25) Complete Reference
5.1 Creating Connections
NWConnection uses completion handlers (pre-async/await).
Basic TLS Connection (WWDC 2018 lines 133-166)
import Network
let connection = NWConnection(
host: NWEndpoint.Host("mail.example.com"),
port: NWEndpoint.Port(integerLiteral: 993),
using: .tls
)
connection.stateUpdateHandler = { [weak self] state in
switch state {
case .ready:
print("Connection established")
self?.sendData()
case .waiting(let error):
print("Waiting for network: \(error)")
case .failed(let error):
print("Connection failed: \(error)")
case .cancelled:
print("Connection cancelled")
default:
break
}
}
connection.start(queue: .main)
Critical Always use [weak self] in stateUpdateHandler to prevent retain cycles.
Custom Parameters
let parameters = NWParameters.tls
parameters.prohibitExpensivePaths = true
parameters.prohibitConstrainedPaths = true
parameters.requiredInterfaceType = .wifi
parameters.ipOptions.version = .v6
let connection = NWConnection(host: "example.com", port: 443, using: parameters)
5.2 State Handling
NWConnection state machine (same as NetworkConnection):
setup → preparing → waiting/ready → failed/cancelled
State handling best practices
connection.stateUpdateHandler = { [weak self] state in
guard let self = self else { return }
switch state {
case .preparing:
self.updateUI(.connecting)
case .waiting(let error):
print("Waiting: \(error.localizedDescription)")
self.updateUI(.waiting)
case .ready:
self.updateUI(.connected)
self.startSending()
case .failed(let error):
print("Failed: \(error.localizedDescription)")
self.updateUI(.failed)
case .cancelled:
self.updateUI(.disconnected)
default:
break
}
}
5.3 Send/Receive with Callbacks
Send with Pacing (WWDC 2018 lines 320-341)
func sendData() {
let data = Data("Hello, world!".utf8)
connection.send(content: data, completion: .contentProcessed { [weak self] error in
if let error = error {
print("Send error: \(error)")
return
}
self?.sendNextData()
})
}
contentProcessed callback Invoked when network stack consumes your data (equivalent to when blocking socket call would return). Use this for pacing to avoid buffering excessive data.
Receive with Exact Byte Count
connection.receive(minimumIncompleteLength: 10, maximumLength: 10) { [weak self] (data, context, isComplete, error) in
if let error = error {
print("Receive error: \(error)")
return
}
if let data = data {
print("Received \(data.count) bytes")
self?.receiveMore()
}
}
Receive parameters
minimumIncompleteLength: Minimum bytes before callback (1 = return any data)
maximumLength: Maximum bytes per callback
- For "exactly n bytes": Set both to n
5.4 UDP Batching (WWDC 2018 lines 343-347)
Batch sending for 30% CPU reduction.
let connection = NWConnection(
host: NWEndpoint.Host("game-server.example.com"),
port: NWEndpoint.Port(integerLiteral: 9000),
using: .udp
)
connection.start(queue: .main)
func sendVideoFrames(_ frames: [Data]) {
connection.batch {
for frame in frames {
connection.send(content: frame, completion: .contentProcessed { error in
if let error = error {
print("Send error: \(error)")
}
})
}
}
}
Without batch 100 datagrams = 100 syscalls = high CPU
With batch 100 datagrams = ~1 syscall = 30% lower CPU (measured with Instruments)
5.5 NWListener (WWDC 2018 lines 233-293)
Accept incoming connections.
import Network
let listener = try NWListener(using: .tcp, on: 1029)
listener.service = NWListener.Service(name: "MyApp", type: "_myapp._tcp")
listener.serviceRegistrationUpdateHandler = { update in
switch update {
case .add(let endpoint):
if case .service(let name, let type, let domain, _) = endpoint {
print("Advertising: \(name).\(type)\(domain)")
}
default:
break
}
}
listener.newConnectionHandler = { [weak self] newConnection in
print("New connection from: \(newConnection.endpoint)")
newConnection.stateUpdateHandler = { state in
if case .ready = state {
print("Client connected")
self?.handleClient(newConnection)
}
}
newConnection.start(queue: .main)
}
listener.stateUpdateHandler { state
state {
.ready:
()
.failed( error):
()
:
}
}
listener.start(queue: .main)
5.6 NWBrowser (Bonjour Discovery)
Discover services on local network.
import Network
let browser = NWBrowser(
for: .bonjour(type: "_http._tcp", domain: nil),
using: .tcp
)
browser.browseResultsChangedHandler = { results, changes in
for result in results {
switch result.endpoint {
case .service(let name, let type, let domain, _):
print("Found service: \(name).\(type)\(domain)")
let connection = NWConnection(to: result.endpoint, using: .tcp)
connection.start(queue: .main)
default:
break
}
}
}
browser.stateUpdateHandler = { state in
switch state {
case .ready:
print("Browser ready")
case .failed(let error):
print("Browser failed: \(error)")
default:
break
}
}
browser.start(queue: .main)
Mobility & Network Transitions
Connection Viability (WWDC 2018 lines 453-463)
Viability = connection can send/receive data (has valid route).
connection.viabilityUpdateHandler = { isViable in
if isViable {
print("✅ Connection viable (can send/receive)")
} else {
print("⚠️ Connection not viable (no route)")
}
}
When viability changes
- Walk into elevator (WiFi signal lost) → not viable
- Walk out of elevator (WiFi returns) → viable again
- Switch WiFi → cellular → not viable briefly → viable on cellular
Best practice Don't tear down connection on viability loss. Framework will recover when network returns.
Better Path Available (WWDC 2018 lines 464-477)
Better path = alternative network with better characteristics.
connection.betterPathUpdateHandler = { betterPathAvailable in
if betterPathAvailable {
print("📶 Better path available (e.g., WiFi while on cellular)")
self.migrateToNewConnection()
}
}
Scenarios
- Connected on cellular, walk into building with WiFi → better path available
- Connected on WiFi, WiFi quality degrades, cellular available → better path available
Migration pattern
func migrateToNewConnection() {
let newConnection = NWConnection(host: host, port: port, using: parameters)
newConnection.stateUpdateHandler = { [weak self] state in
if case .ready = state {
self?.currentConnection?.cancel()
self?.currentConnection = newConnection
}
}
newConnection.start(queue: .main)
}
Multipath TCP (WWDC 2018 lines 480-487)
Automatically migrate between networks without application intervention.
let parameters = NWParameters.tcp
parameters.multipathServiceType = .handover
let connection = NWConnection(host: "example.com", port: 443, using: parameters)
Multipath TCP modes
.handover — Seamless handoff between networks (WiFi ↔ cellular)
.interactive — Use multiple paths simultaneously (lowest latency)
.aggregate — Use multiple paths simultaneously (highest throughput)
Benefits
- Automatic network transition (no viability handlers needed)
- No connection interruption when switching networks
- Fallback to single-path if MPTCP unavailable
NWPathMonitor (WWDC 2018 lines 489-496)
Monitor network state changes (replaces SCNetworkReachability).
import Network
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
print("✅ Network available")
if path.usesInterfaceType(.wifi) {
print("Using WiFi")
} else if path.usesInterfaceType(.cellular) {
print("Using cellular")
}
if path.isExpensive {
print("⚠️ Expensive path (cellular/hotspot)")
}
} else {
print("❌ No network")
}
}
monitor.start(queue: .main)
Use cases
- Show "No network" UI when path.status == .unsatisfied
- Disable high-bandwidth features when path.isExpensive
- Adjust quality based on interface type
When to use
- Global network state monitoring
- When "waiting for connectivity" isn't enough
- Need to know available interfaces before connecting
When NOT to use
- Checking before connecting (use waiting state instead)
- Per-connection monitoring (use viability handlers instead)
Security Configuration
TLS Version
let tlsOptions = NWProtocolTLS.Options()
tlsOptions.minimumTLSProtocolVersion = .TLSv12
tlsOptions.minimumTLSProtocolVersion = .TLSv13
let parameters = NWParameters(tls: tlsOptions)
let connection = NWConnection(host: "example.com", port: 443, using: parameters)
Certificate Pinning
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(
tlsOptions.securityProtocolOptions,
{ (metadata, trust, complete) in
let serverCert = sec_protocol_metadata_copy_peer_public_key(metadata)
let pinnedCertData = Data()
let serverCertData = SecCertificateCopyData(serverCert) as Data
if serverCertData == pinnedCertData {
complete(true)
} else {
complete(false)
}
},
.main
)
let parameters = NWParameters(tls: tlsOptions)
Certificate Pinning + Corporate Proxies
Corporate networks often use TLS inspection proxies that present their own certificates. Strict pinning breaks these environments.
Strategy: Pin against the public key (SPKI) rather than the full certificate, and provide a configuration escape hatch:
sec_protocol_options_set_verify_block(
tlsOptions.securityProtocolOptions,
{ (metadata, trust, complete) in
let secTrust = sec_trust_copy_ref(trust).takeRetainedValue()
SecTrustEvaluateAsyncWithError(secTrust, .main) { _, result, _ in
guard result else { complete(false); return }
if PinningConfig.isEnabled {
let serverKey = SecTrustCopyKey(secTrust)
let matches = pinnedKeys.contains { $0 == serverKey }
complete(matches)
} else {
complete(true)
}
}
},
.main
)
Rules:
- Always validate system trust first (
SecTrustEvaluateAsyncWithError) — this respects enterprise-installed root CAs
- Use public key pinning over certificate pinning (survives cert rotation)
- Provide a managed configuration (MDM profile or app config) to disable pinning in enterprise environments
- Pin at least 2 keys (current + backup) to survive rotation
Cipher Suites
let tlsOptions = NWProtocolTLS.Options()
tlsOptions.tlsCipherSuites = [
tls_ciphersuite_t(rawValue: 0x1301),
tls_ciphersuite_t(rawValue: 0x1302),
]
Performance Optimization
User-Space Networking (WWDC 2018 lines 409-441)
Automatic on iOS/tvOS. Network.framework moves TCP/UDP stack into your app process.
Benefits
- ~30% lower CPU usage (measured with Instruments)
- No kernel→userspace copy (memory-mapped regions)
- Reduced context switches
Legacy vs User-Space
| Traditional Sockets | User-Space Networking |
|---|
| Packet → driver → kernel → copy → userspace | Packet → driver → memory-mapped region → userspace (no copy) |
| 100 datagrams = 100 syscalls | 100 datagrams = ~1 syscall (with batching) |
| ~30% higher CPU | Baseline CPU |
WWDC demo Live UDP video streaming showed 30% CPU difference (sockets vs Network.framework).
ECN for UDP (WWDC 2018 lines 365-378)
Explicit Congestion Notification for smooth UDP transmission.
let ipMetadata = NWProtocolIP.Metadata()
ipMetadata.ecnFlag = .congestionEncountered
let context = NWConnection.ContentContext(
identifier: "video_frame",
metadata: [ipMetadata]
)
connection.send(content: data, contentContext: context, completion: .contentProcessed { _ in })
ECN flags
.ect0 / .ect1 — ECN-capable transport
.congestionEncountered — Congestion notification received
Benefits Network can signal congestion without dropping packets.
Service Class (WWDC 2018 lines 379-388)
Mark traffic priority.
let parameters = NWParameters.tcp
parameters.serviceClass = .background
let connection = NWConnection(host: "example.com", port: 443, using: parameters)
let ipMetadata = NWProtocolIP.Metadata()
ipMetadata.serviceClass = .realTimeInteractive
let context = NWConnection.ContentContext(identifier: "voip", metadata: [ipMetadata])
connection.send(content: audioData, contentContext: context, completion: .contentProcessed { _ in })
Service classes
.background — Low priority (large downloads, sync)
.default — Normal priority
.responsiveData — Interactive data (API calls)
.realTimeInteractive — Time-sensitive (voice, gaming)
TCP Fast Open (WWDC 2018 lines 389-406)
Send initial data in TCP SYN packet (saves round trip).
let parameters = NWParameters.tcp
parameters.allowFastOpen = true
let connection = NWConnection(host: "example.com", port: 443, using: parameters)
let initialData = Data("GET / HTTP/1.1\r\n".utf8)
connection.send(
content: initialData,
contentContext: .defaultMessage,
isComplete: false,
completion: .idempotent
)
connection.start(queue: .main)
Benefits Reduces connection establishment time by 1 RTT.
Requirements Data must be idempotent (safe to replay if SYN retransmitted).
Migration Strategies
From BSD Sockets to NWConnection
| BSD Sockets | NWConnection | Notes |
|---|
socket() + connect() | NWConnection(host:port:using:) + start() | Non-blocking by default |
send() / sendto() | connection.send(content:completion:) | Async callback |
recv() / recvfrom() | connection.receive(min:max:completion:) | Async callback |
bind() + listen() | NWListener(using:on:) | Automatic port binding |
accept() | listener.newConnectionHandler | Callback per connection |
getaddrinfo() | Use NWEndpoint.Host(hostname) | DNS automatic |
SCNetworkReachability | connection.stateUpdateHandler waiting state | No race conditions |
setsockopt() | NWParameters | Type-safe options |
Migration example
Before (blocking sockets)
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, &addr, addrlen);
send(sock, data, len, 0);
After (NWConnection)
let connection = NWConnection(host: "example.com", port: 443, using: .tls)
connection.stateUpdateHandler = { state in
if case .ready = state {
connection.send(content: data, completion: .contentProcessed { _ in })
}
}
connection.start(queue: .main)
From URLSession StreamTask to NetworkConnection
When to migrate
- Need UDP (StreamTask only supports TCP)
- Need custom protocols
- Need low-level control
When to STAY with URLSession
- HTTP/HTTPS (URLSession optimized for this)
- WebSocket support
- Built-in caching, cookies
Migration example
Before (URLSession StreamTask)
let task = URLSession.shared.streamTask(withHostName: "example.com", port: 443)
task.resume()
task.write(Data("Hello".utf8), timeout: 10) { _ in }
After (NetworkConnection iOS 26+)
let connection = NetworkConnection(to: .hostPort(host: "example.com", port: 443)) { TLS() }
try await connection.send(Data("Hello".utf8))
From NWConnection to NetworkConnection
Benefits of migration
- Async/await (no callback nesting)
- No
[weak self] needed
- TLV framing built-in
- Coder protocol for Codable types
Migration mapping
| NWConnection | NetworkConnection |
|---|
connection.stateUpdateHandler = { } | for await state in connection.states { } |
connection.send(content:completion:) | try await connection.send(content) |
connection.receive(min:max:completion:) | try await connection.receive(exactly:) |
| Manual JSON | Coder(MyType.self, using: .json) |
| Custom framer | TLV { TLS() } |
[weak self] everywhere | No [weak self] needed |
Migration example
Before (NWConnection)
connection.stateUpdateHandler = { [weak self] state in
if case .ready = state {
self?.sendData()
}
}
func sendData() {
connection.send(content: data, completion: .contentProcessed { [weak self] error in
self?.receiveData()
})
}
After (NetworkConnection)
Task {
for await state in connection.states {
if case .ready = state {
try await connection.send(data)
let received = try await connection.receive(exactly: 10).content
}
}
}
Testing Checklist
Before shipping networking code:
Device Testing
Network Conditions
Network Types
Performance
Error Handling
iOS 26+ Features (if using NetworkConnection)
API Quick Reference
NetworkConnection (iOS 26+)
NetworkConnection(to: .hostPort(host: "example.com", port: 443)) { TLS() }
try await connection.send(data)
try await connection.receive(exactly: n).content
for await state in connection.states { }
NetworkConnection(to: endpoint) { TLV { TLS() } }
NetworkConnection(to: endpoint) { Coder(MyType.self, using: .json) { TLS() } }
NetworkListener { TLS() }.run { connection in }
NetworkBrowser(for: .wifiAware(...)).run { endpoints in }
NWConnection (iOS 12-25)
let connection = NWConnection(host: "example.com", port: 443, using: .tls)
connection.stateUpdateHandler = { [weak self] state in }
connection.start(queue: .main)
connection.send(content: data, completion: .contentProcessed { [weak self] error in })
connection.receive(minimumIncompleteLength: min, maximumLength: max) { [weak self] data, context, isComplete, error in }
connection.viabilityUpdateHandler = { isViable in }
connection.betterPathUpdateHandler = { betterPathAvailable in }
connection.cancel()
NWListener (iOS 12-25)
let listener = try NWListener(using: .tcp, on: 1029)
listener.newConnectionHandler = { newConnection in }
listener.start(queue: .main)
NWBrowser (iOS 12-25)
let browser = NWBrowser(for: .bonjour(type: "_http._tcp", domain: nil), using: .tcp)
browser.browseResultsChangedHandler = { results, changes in }
browser.start(queue: .main)
NWPathMonitor
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in }
monitor.start(queue: .main)
Resources
WWDC: 2018-715, 2025-250
Docs: /network, /network/nwconnection, /network/networkconnection
Skills: axiom-networking, axiom-networking-diag
Last Updated 2025-12-02
Status Production-ready reference from WWDC 2018 and WWDC 2025
Coverage NWConnection (iOS 12-25), NetworkConnection (iOS 26+), all 12 WWDC 2025 code examples