| 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
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?.sendInitialData()
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)
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?.sendNextChunk()
})
}
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")
self?.receiveData()
}
}
}
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)
for frame in videoFrames {
sendto(socket, frame.bytes, frame.count, 0, &addr, addrlen)
}
GOOD: Batched UDP Sends (30% Lower CPU)
import Network
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)
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)")
}
})
}
}
}
func receiveFrames() {
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] (data, context, isComplete, error) in
if let error = error {
()
}
data data {
.displayFrame(data)
.receiveFrames()
}
}
}
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
let sock = socket(AF_INET, SOCK_STREAM, 0)
bind(sock, &addr, addrlen)
listen(sock, 5)
while true {
let client = accept(sock, nil, nil)
}
GOOD: NWListener with Automatic Connection Handling
import Network
let listener = try NWListener(using: .tcp, on: 1029)
listener.service = NWListener.Service(name: "MyApp", type: "_myservice._tcp")
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
}
}
listener.newConnectionHandler = { [weak self] newConnection in
print("New connection from: \(newConnection.endpoint)")
newConnection.stateUpdateHandler = { state in
switch state {
case .ready:
print("Client connected")
.handleClient(newConnection)
.failed( error):
()
:
}
}
newConnection.start(queue: .main)
}
listener.stateUpdateHandler { state
state {
.ready:
()
.failed( error):
()
:
}
}
listener.start(queue: .main)
( : ) {
connection.receive(minimumIncompleteLength: , maximumLength: ) { [ ] (data, context, isComplete, error)
error error {
()
}
data data {
()
connection.send(content: data, completion: .contentProcessed { error
error error {
()
}
})
.handleClient(connection)
}
}
}
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
let connection = NWConnection(host: "192.168.1.100", port: 9000, using: .tcp)
GOOD: NWBrowser for Service Discovery
import Network
let browser = NWBrowser(for: .bonjour(type: "_myservice._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)")
self.connectToService(result.endpoint)
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)
func connectToService(_ endpoint: ) {
connection (to: endpoint, using: .tcp)
connection.stateUpdateHandler { state
.ready state {
()
}
}
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