---
name: axiom-networking-legacy
description: This skill should be used when working with NWConnection patterns for iOS 12-25, supporting apps that can't use async/await yet, or maintaining backward compatibility with completion handler networking.
license: MIT
---

# Legacy iOS 12-25 NWConnection Patterns

These patterns use NWConnection with completion handlers for apps supporting iOS 12-25. If your app targets iOS 26+, use NetworkConnection with async/await instead (see axiom-network-framework-ref skill).

## Pattern 2a: NWConnection with TLS (iOS 12-25)

**Use when** Supporting iOS 12-25, need TLS encryption, can't use async/await yet

**Time cost** 10-15 minutes

### GOOD: NWConnection with Completion Handlers

```swift
import Network

// Create connection with TLS
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?.sendInitialData()
    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)

// Send data with 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 callback = network stack consumed data
        // This is when you should send next chunk (pacing)
        self?.sendNextChunk()
    })
}

// Receive exact byte count
func receiveData() {
    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...
            self?.receiveData() // Continue receiving
        }
    }
}
```

### Key differences from NetworkConnection
- Must use `[weak self]` in all completion handlers to prevent retain cycles
- stateUpdateHandler receives state, not async sequence
- send/receive use completion callbacks, not async/await

### When to use
- Supporting iOS 12-15 (70% of devices as of 2024)
- Codebases not yet using async/await
- Libraries needing backward compatibility

### Migration to NetworkConnection (iOS 26+)
- stateUpdateHandler -> connection.states async sequence
- Completion handlers -> try await calls
- [weak self] -> No longer needed (async/await handles cancellation)

## Pattern 2b: NWConnection UDP Batch (iOS 12-25)

**Use when** Supporting iOS 12-25, sending multiple UDP datagrams efficiently, need ~30% CPU reduction

**Time cost** 10-15 minutes

**Background** Traditional UDP sockets send one datagram per syscall. If you're sending 100 small packets, that's 100 context switches. Batching reduces this to ~1 syscall.

### BAD: Individual UDP Sends (High CPU)
```swift
// WRONG — 100 context switches for 100 packets
for frame in videoFrames {
    sendto(socket, frame.bytes, frame.count, 0, &addr, addrlen)
    // Each send = context switch to kernel
}
```

### GOOD: Batched UDP Sends (30% Lower CPU)

```swift
import Network

// UDP connection
let connection = NWConnection(
    host: NWEndpoint.Host("stream-server.example.com"),
    port: NWEndpoint.Port(integerLiteral: 9000),
    using: .udp
)

connection.stateUpdateHandler = { state in
    if case .ready = state {
        print("Ready to send UDP")
    }
}

connection.start(queue: .main)

// Batch sending for efficiency
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
    // 30% lower CPU usage vs individual sends
}

// Receive UDP datagrams
func receiveFrames() {
    connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] (data, context, isComplete, error) in
        if let error = error {
            print("Receive error: \(error)")
            return
        }

        if let data = data {
            // Process video frame
            self?.displayFrame(data)
            self?.receiveFrames() // Continue receiving
        }
    }
}
```

### Performance characteristics
- **Without batch** 100 datagrams = 100 syscalls = 100 context switches
- **With batch** 100 datagrams = ~1 syscall = 1 context switch
- **Result** ~30% lower CPU usage (measured with Instruments)

### When to use
- Real-time video/audio streaming
- Gaming with frequent updates (player position)
- High-frequency sensor data (IoT)

**WWDC 2018 demo** Live video streaming showed 30% lower CPU on receiver with user-space networking + batching

## Pattern 2c: NWListener (iOS 12-25)

**Use when** Need to accept incoming connections, building servers or peer-to-peer apps, supporting iOS 12-25

**Time cost** 20-25 minutes

### BAD: Manual Socket Listening
```swift
// WRONG — Manual socket management
let sock = socket(AF_INET, SOCK_STREAM, 0)
bind(sock, &addr, addrlen)
listen(sock, 5)
while true {
    let client = accept(sock, nil, nil) // Blocks thread
    // Handle client...
}
```

### GOOD: NWListener with Automatic Connection Handling

```swift
import Network

// Create listener with default parameters
let listener = try NWListener(using: .tcp, on: 1029)

// Advertise Bonjour service
listener.service = NWListener.Service(name: "MyApp", type: "_myservice._tcp")

// Handle service registration updates
listener.serviceRegistrationUpdateHandler = { update in
    switch update {
    case .add(let endpoint):
        if case .service(let name, let type, let domain, _) = endpoint {
            print("Advertising as: \(name).\(type)\(domain)")
        }
    default:
        break
    }
}

// Handle incoming connections
listener.newConnectionHandler = { [weak self] newConnection in
    print("New connection from: \(newConnection.endpoint)")

    // Configure connection
    newConnection.stateUpdateHandler = { state in
        switch state {
        case .ready:
            print("Client connected")
            self?.handleClient(newConnection)
        case .failed(let error):
            print("Client connection failed: \(error)")
        default:
            break
        }
    }

    // Start handling this connection
    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)

// Handle client data
func handleClient(_ connection: NWConnection) {
    connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [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")

            // Echo back
            connection.send(content: data, completion: .contentProcessed { error in
                if let error = error {
                    print("Send error: \(error)")
                }
            })

            self?.handleClient(connection) // Continue receiving
        }
    }
}
```

### When to use
- Peer-to-peer apps (file sharing, messaging)
- Local network services
- Development/testing servers

### Bonjour advertising
- Automatic service discovery on local network
- No hardcoded IPs needed
- Works with NWBrowser for discovery

### Security considerations
- Use TLS parameters for encryption: `NWListener(using: .tls, on: port)`
- Validate client connections before processing data
- Set connection limits to prevent DoS

## Pattern 2d: Network Discovery (iOS 12-25)

**Use when** Discovering services on local network (Bonjour), building peer-to-peer apps, supporting iOS 12-25

**Time cost** 25-30 minutes

### BAD: Hardcoded IP Addresses
```swift
// WRONG — Brittle, requires manual configuration
let connection = NWConnection(host: "192.168.1.100", port: 9000, using: .tcp)
// What if IP changes? What if multiple devices?
```

### GOOD: NWBrowser for Service Discovery

```swift
import Network

// Browse for services on local network
let browser = NWBrowser(for: .bonjour(type: "_myservice._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
            self.connectToService(result.endpoint)
        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)

// Connect to discovered service
func connectToService(_ endpoint: NWEndpoint) {
    let connection = NWConnection(to: endpoint, using: .tcp)

    connection.stateUpdateHandler = { state in
        if case .ready = state {
            print("Connected to service")
        }
    }

    connection.start(queue: .main)
}
```

### When to use
- Peer-to-peer discovery (AirDrop-like features)
- Local network printers, media servers
- Development/testing (find test servers automatically)

### Performance characteristics
- mDNS-based (multicast DNS, no central server)
- Near-instant discovery on same subnet
- Automatic updates when services appear/disappear

### iOS 26+ alternative
- Use NetworkBrowser with Wi-Fi Aware for peer-to-peer without infrastructure
- See Pattern 1d in axiom-network-framework-ref skill

## Resources

**Skills**: axiom-networking, axiom-network-framework-ref, axiom-networking-migration
