| name | 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 |
| version | 1.0.0 |
| skill_type | reference |
| apple_platforms | iOS 12+ (NWConnection), iOS 26+ (NetworkConnection) |
| last_updated | Tue Dec 02 2025 00:00:00 GMT+0000 (Coordinated Universal Time) |
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
networkingfor anti-patterns, common patterns, pressure scenarios - Use
networking-diagfor 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
// Basic connection with TLS (TCP and IP inferred)
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
TLS()
}
// Send and receive with async/await
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()infersTCP()andIP()automatically- No explicit connection.start() needed (happens on first send/receive)
- Async/await eliminates callback nesting
Example 2: Custom IP Options (WWDC 4:41)
// Customize IP fragmentation
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
TLS {
TCP {
IP()
.fragmentationEnabled(false) // Disable IP fragmentation
}
}
}
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)
// Constrained paths (low data mode) + custom IP
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029),
using: .parameters {
TLS {
TCP {
IP()
.fragmentationEnabled(false)
}
}
}
.constrainedPathsProhibited(true) // Don't use cellular in low data mode
)
Common parameters
.constrainedPathsProhibited(true)— Respect low data mode.expensivePathsProhibited(true)— Don't use cellular/hotspot.multipathServiceType(.handover)— Enable Multipath TCP
Endpoint Types
// Host + Port
.hostPort(host: "example.com", port: 443)
// Service (Bonjour)
.service(name: "MyPrinter", type: "_ipp._tcp", domain: "local.", interface: nil)
// Unix domain socket
.unix(path: "/tmp/my.sock")
Protocol Stack Composition
// TLS over TCP (most common)
TLS()
// QUIC (TLS + UDP, multiplexed streams)
QUIC()
// UDP (datagrams)
UDP()
// TCP (stream, no encryption)
TCP()
// WebSocket over TLS
WebSocket {
TLS()
}
// Custom framing
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
// Option 1: Async sequence (monitor in background)
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)
// Receive exactly 98 bytes
let incomingData = try await connection.receive(exactly: 98).content
print("Received \(incomingData.count) bytes")
Receive: Variable Length (WWDC 8:29)
// Read UInt32 length prefix, then read that many bytes
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
// Process chunk...
}
receive() variants
receive(exactly: n)— Wait for exactly n bytesreceive(atLeast: min, atMost: max)— Get between min and max bytesreceive(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
// Define message types
enum GameMessage: Int {
case selectedCharacter = 0
case move = 1
}
struct GameCharacter: Codable {
let character: String
}
struct GameMove: Codable {
let row: Int
let column: Int
}
// Connection with TLV framing
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
TLV {
TLS()
}
}
// Send typed message
public func sendWithTLV() async throws {
let characterData = try JSONEncoder().encode(GameCharacter(character: "🐨"))
try await connection.send(characterData, type: GameMessage.selectedCharacter.rawValue)
}
// Receive typed message
public func receiveWithTLV() async throws {
let (incomingData, metadata) = try await connection.receive()
switch GameMessage(rawValue: metadata.type) {
case .selectedCharacter:
let character = try JSONDecoder().decode(GameCharacter.self, from: incomingData)
print("Character selected: \(character)")
case .move:
let move = try JSONDecoder().decode(GameMove.self, from: incomingData)
print("Move: row=\(move.row), column=\(move.column)")
case .none:
print("Unknown message type: \(metadata.type)")
}
}
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
// Define message types as Codable enum
enum GameMessage: Codable {
case selectedCharacter(String)
case move(row: Int, column: Int)
}
// Connection with Coder
let connection = NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
Coder(GameMessage.self, using: .json) {
TLS()
}
}
// Send Codable directly (no encoding needed!)
public func sendWithCoder() async throws {
let selectedCharacter: GameMessage = .selectedCharacter("🐨")
try await connection.send(selectedCharacter)
}
// Receive Codable directly (no decoding needed!)
public func receiveWithCoder() async throws {
let gameMessage = try await connection.receive().content // Returns GameMessage!
switch gameMessage {
case .selectedCharacter(let character):
print("Character selected: \(character)")
case .move(let row, let column):
print("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
// Listener with Coder protocol
public func listenForIncomingConnections() async throws {
try await NetworkListener {
Coder(GameMessage.self, using: .json) {
TLS()
}
}.run { connection in
// Each connection gets its own subtask
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.messagesasync sequence for receiving
Listener configuration
// Specify port
NetworkListener(port: 1029) { TLS() }
// Let system choose port
NetworkListener { TLS() }
// Bonjour advertising
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
// Browse for nearby paired Wi-Fi Aware devices
public func findNearbyDevice() async throws {
let endpoint = try await NetworkBrowser(
for: .wifiAware(.connecting(to: .allPairedDevices, from: .ticTacToeService))
).run { endpoints in
.finish(endpoints.first!) // Use first discovered device
}
// Make connection to the discovered endpoint
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, high throughput
- iOS 26+ only
Browse descriptors
// Bonjour
.bonjour(type: "_http._tcp", domain: "local")
// Wi-Fi Aware (all paired devices)
.wifiAware(.connecting(to: .allPairedDevices, from: .myService))
// Wi-Fi Aware (specific device)
.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
// Create connection
let connection = NWConnection(
host: NWEndpoint.Host("mail.example.com"),
port: NWEndpoint.Port(integerLiteral: 993),
using: .tls // TCP inferred
)
// Handle connection state changes
connection.stateUpdateHandler = { [weak self] state in
switch state {
case .ready:
print("Connection established")
self?.sendData()
case .waiting(let error):
print("Waiting for network: \(error)")
// Show "Waiting..." UI, don't fail immediately
case .failed(let error):
print("Connection failed: \(error)")
case .cancelled:
print("Connection cancelled")
default:
break
}
}
// Start connection
connection.start(queue: .main)
Critical Always use [weak self] in stateUpdateHandler to prevent retain cycles.
Custom Parameters
// Create custom parameters
let parameters = NWParameters.tls
// Prohibit expensive networks
parameters.prohibitExpensivePaths = true // Don't use cellular/hotspot
// Prohibit constrained networks
parameters.prohibitConstrainedPaths = true // Respect low data mode
// Require IPv6
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:
// DNS lookup, TCP SYN, TLS handshake in progress
self.updateUI(.connecting)
case .waiting(let error):
// Network unavailable or blocked
// DON'T fail immediately, framework retries automatically
print("Waiting: \(error.localizedDescription)")
self.updateUI(.waiting)
case .ready:
// Connection established, can send/receive
self.updateUI(.connected)
self.startSending()
case .failed(let error):
// Unrecoverable error after all retry attempts
print("Failed: \(error.localizedDescription)")
self.updateUI(.failed)
case .cancelled:
// connection.cancel() called
self.updateUI(.disconnected)
default:
break
}
}
5.3 Send/Receive with Callbacks
Send with Pacing (WWDC 2018 lines 320-341)
// Send with contentProcessed callback for pacing
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
}
// contentProcessed = network stack consumed data
// NOW send next chunk (pacing)
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
// Receive exactly 10 bytes
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")
// Process data...
// Continue receiving
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.
// UDP connection
let connection = NWConnection(
host: NWEndpoint.Host("game-server.example.com"),
port: NWEndpoint.Port(integerLiteral: 9000),
using: .udp
)
connection.start(queue: .main)
// Batch multiple datagrams
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)")
}
})
}
}
// All sends batched into ~1 syscall
// Result: 30% lower CPU usage vs individual sends
}
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
// Create listener on port 1029
let listener = try NWListener(using: .tcp, on: 1029)
// Advertise Bonjour service
listener.service = NWListener.Service(name: "MyApp", type: "_myapp._tcp")
// Handle service registration
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
}
}
// Handle new connections
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)
}
// Handle listener state
listener.stateUpdateHandler = { state in
switch state {
case .ready:
print("Listener ready on port \(listener.port ?? 0)")
case .failed(let error):
print("Listener failed: \(error)")
default:
break
}
}
// Start listening
listener.start(queue: .main)
5.6 NWBrowser (Bonjour Discovery)
Discover services on local network.
import Network
// Browse for Bonjour services
let browser = NWBrowser(
for: .bonjour(type: "_http._tcp", domain: nil),
using: .tcp
)
// Handle discovered services
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)")
// Connect to this service
let connection = NWConnection(to: result.endpoint, using: .tcp)
connection.start(queue: .main)
default:
break
}
}
}
// Handle browser state
browser.stateUpdateHandler = { state in
switch state {
case .ready:
print("Browser ready")
case .failed(let error):
print("Browser failed: \(error)")
default:
break
}
}
// Start browsing
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)")
// Don't tear down immediately, may recover
// Show UI: "Connection interrupted"
}
}
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)")
// Consider migrating to new connection
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() {
// Create new connection
let newConnection = NWConnection(host: host, port: port, using: parameters)
newConnection.stateUpdateHandler = { [weak self] state in
if case .ready = state {
// New connection ready, switch over
self?.currentConnection?.cancel()
self?.currentConnection = newConnection
}
}
newConnection.start(queue: .main)
// Keep old connection until new one ready
}
Multipath TCP (WWDC 2018 lines 480-487)
Automatically migrate between networks without application intervention.
let parameters = NWParameters.tcp
parameters.multipathServiceType = .handover // Seamless network transition
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")
// Check interface types
if path.usesInterfaceType(.wifi) {
print("Using WiFi")
} else if path.usesInterfaceType(.cellular) {
print("Using cellular")
}
// Check if expensive
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
// iOS 13+ requires TLS 1.2+ by default
let tlsOptions = NWProtocolTLS.Options()
// Allow TLS 1.2 and 1.3
tlsOptions.minimumTLSProtocolVersion = .TLSv12
// Require TLS 1.3 only
tlsOptions.minimumTLSProtocolVersion = .TLSv13
let parameters = NWParameters(tls: tlsOptions)
let connection = NWConnection(host: "example.com", port: 443, using: parameters)
Certificate Pinning
// Production-grade certificate pinning
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(
tlsOptions.securityProtocolOptions,
{ (metadata, trust, complete) in
// Get server certificate
let serverCert = sec_protocol_metadata_copy_peer_public_key(metadata)
// Compare with pinned certificate
let pinnedCertData = Data(/* your pinned cert */)
let serverCertData = SecCertificateCopyData(serverCert) as Data
if serverCertData == pinnedCertData {
complete(true) // Accept
} else {
complete(false) // Reject (prevents MITM attacks)
}
},
.main
)
let parameters = NWParameters(tls: tlsOptions)
Cipher Suites
let tlsOptions = NWProtocolTLS.Options()
// Specify allowed cipher suites
tlsOptions.tlsCipherSuites = [
tls_ciphersuite_t(rawValue: 0x1301), // TLS_AES_128_GCM_SHA256
tls_ciphersuite_t(rawValue: 0x1302), // TLS_AES_256_GCM_SHA384
]
// iOS defaults to secure modern ciphers, only customize if required
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.
// Create IP metadata with ECN
let ipMetadata = NWProtocolIP.Metadata()
ipMetadata.ecnFlag = .congestionEncountered // Or .ect0, .ect1
// Attach to send context
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.
// Connection-wide service class
let parameters = NWParameters.tcp
parameters.serviceClass = .background // Low priority
let connection = NWConnection(host: "example.com", port: 443, using: parameters)
// Per-packet service class (UDP)
let ipMetadata = NWProtocolIP.Metadata()
ipMetadata.serviceClass = .realTimeInteractive // High priority (voice)
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)
// Send initial data BEFORE calling start()
let initialData = Data("GET / HTTP/1.1\r\n".utf8)
connection.send(
content: initialData,
contentContext: .defaultMessage,
isComplete: false,
completion: .idempotent // Data is safe to replay
)
// Now start connection (initial data sent in SYN)
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); // BLOCKS
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
- Tested on real device (not just simulator)
- Tested on multiple iOS versions (12, 15, 26)
- Tested on iPhone and iPad (different network characteristics)
Network Conditions
- WiFi (home network)
- Cellular (disable WiFi)
- Airplane Mode → WiFi (test waiting state)
- WiFi → cellular transition (walk out of building)
- Cellular → WiFi transition (walk into building)
- Weak signal (basement, elevator)
- Network Link Conditioner (100ms latency, 3% packet loss)
Network Types
- IPv4-only network
- IPv6-only network (some cellular carriers)
- Dual-stack (IPv4 + IPv6)
- Corporate VPN active
- Personal hotspot (expensive path)
Performance
- Connection establishment < 500ms (check logs)
- Using batch for UDP (verify with Instruments)
- Using contentProcessed for pacing (check send timing)
- Profiled with Instruments Network template
- CPU usage acceptable (< 10% for networking)
- Memory stable (no leaks, check [weak self])
Error Handling
- Handling .waiting state (show "Waiting..." UI)
- Handling .failed state (specific error messages)
- TLS handshake errors logged
- Timeout handling (don't wait forever)
- User-facing errors actionable ("Check network" not "POSIX 61")
iOS 26+ Features (if using NetworkConnection)
- Using TLV framing if need message boundaries
- Using Coder protocol if sending Codable types
- Using NetworkListener instead of NWListener
- Using NetworkBrowser for Wi-Fi Aware if peer-to-peer
API Quick Reference
NetworkConnection (iOS 26+)
// Create connection
NetworkConnection(to: .hostPort(host: "example.com", port: 443)) { TLS() }
// Send
try await connection.send(data)
// Receive
try await connection.receive(exactly: n).content
// States
for await state in connection.states { }
// TLV framing
NetworkConnection(to: endpoint) { TLV { TLS() } }
// Coder protocol
NetworkConnection(to: endpoint) { Coder(MyType.self, using: .json) { TLS() } }
// Listener
NetworkListener { TLS() }.run { connection in }
// Browser
NetworkBrowser(for: .wifiAware(...)).run { endpoints in }
NWConnection (iOS 12-25)
// Create connection
let connection = NWConnection(host: "example.com", port: 443, using: .tls)
// State handler
connection.stateUpdateHandler = { [weak self] state in }
// Start
connection.start(queue: .main)
// Send
connection.send(content: data, completion: .contentProcessed { [weak self] error in })
// Receive
connection.receive(minimumIncompleteLength: min, maximumLength: max) { [weak self] data, context, isComplete, error in }
// Viability
connection.viabilityUpdateHandler = { isViable in }
// Better path
connection.betterPathUpdateHandler = { betterPathAvailable in }
// Cancel
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)
Related Resources
WWDC Sessions
- WWDC 2018/715 "Introducing Network.framework: A modern alternative to Sockets" (NWConnection, user-space networking, deprecations)
- WWDC 2025/250 "Use structured concurrency with Network framework" (NetworkConnection, TLV, Coder, Wi-Fi Aware)
Apple Documentation
- Network Framework Documentation
- NWConnection
- NetworkConnection (iOS 26+)
- Building a Custom Peer-to-Peer Protocol
Related Axiom Skills
- networking — Discipline-enforcing patterns with anti-patterns, pressure scenarios
- networking-diag — Systematic troubleshooting for connection failures, timeouts, performance issues
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