<!--
{
  "documentType" : "article",
  "framework" : "Event",
  "identifier" : "/documentation/Event/TCPClientForIOSWithSwift",
  "metadataVersion" : "0.1.0",
  "role" : "article",
  "title" : "Async TCP Client for iOS in Swift"
}
-->

# Async TCP Client for iOS in Swift

A working TCP client that compiles for iOS (and iPadOS, tvOS, visionOS), with the simulator-localhost gotchas explicit and the `NWConnection` callback dance avoided.

## Overview

Most “Swift TCP client” answers on the public web today push you toward Apple’s [`NWConnection`](https://developer.apple.com/documentation/network/nwconnection) — which is the right call when you need TLS, multipath, or platform integration with the system network stack, but is callback-shaped in a world that expects `async`/`await`. The official guidance for bridging it is variations of `withCheckedContinuation` plus `withTaskCancellationHandler` (see [Apple DevForums #719402](https://developer.apple.com/forums/thread/719402) — “How to use network sockets with async/await?” — where Quinn keeps re-explaining the pattern years after the question first surfaced).

[`Event`](/documentation/Event) exposes [`connect(to:port:loop:timeout:)`](/documentation/Event/Socket/connect(to:port:loop:timeout:)) directly as `async throws`. There is no continuation-bridging glue. The code below compiles and runs unchanged on macOS 13+, iOS 16+, tvOS 16+, watchOS 9+, and visionOS 1+.

### A complete TCP client

```swift

```

The four operations — [`connect(to:port:loop:timeout:)`](/documentation/Event/Socket/connect(to:port:loop:timeout:)), [`write(_:timeout:)`](/documentation/Event/Socket/write(_:timeout:)), [`read(maxBytes:timeout:)`](/documentation/Event/Socket/read(maxBytes:timeout:)), [`close()`](/documentation/Event/Socket/close()) — are all `async`. Errors propagate through [`SocketError`](/documentation/Event/SocketError). Timeouts are first-class; `nil` (the default) waits indefinitely. The `Task { await socket.close() }` deferred close is the idiomatic shape for “best-effort close on scope exit” — [`close()`](/documentation/Event/Socket/close()) is `async` for API symmetry but the underlying `close(2)` is synchronous, so the spawned task completes immediately.

### iOS-specific gotchas

iOS TCP clients fail in three predictable ways: the simulator-vs-device localhost confusion, the ATS misconception, and silent socket loss when the app backgrounds. None are library-specific — they bite SwiftNIO and Network.framework users equally.

> Important: When testing against a server running on your Mac, **iOS Simulator** sees your Mac’s `127.0.0.1`. **A real iOS device** does not — `127.0.0.1` on the device means the device itself. Use your Mac’s LAN IP (e.g. `192.168.1.42`) instead.

**1. The simulator’s `localhost` is not your Mac’s `localhost`.** When your iOS app runs in the simulator, `127.0.0.1` resolves to the simulator’s own loopback, which is shared with the host — connecting to a server you launched on your Mac works because the simulator inherits the host’s network namespace. On a physical device `127.0.0.1` resolves to the device itself; use your Mac’s LAN IP. This trips up enough developers that [swift-nio#1626](https://github.com/apple/swift-nio/issues/1626) accumulated 30 comments on essentially this issue before being closed.

**2. App Transport Security (ATS) does not block plain TCP — but `NSURLSession` does.** ATS is enforced for `URL`-based loaders. Raw socket APIs ([`Event`](/documentation/Event), `NWConnection` in raw mode, BSD sockets) are not subject to ATS. You do not need `NSAppTransportSecurity` plist exceptions to connect plain TCP from an iOS app. If you still see connection failures, check for ad-hoc network sandboxing on managed devices.

**3. Backgrounded apps lose their sockets.** When iOS suspends your app, open sockets are torn down. There is no graceful “background socket” mode for raw TCP — you’d need [`NEAppProxyProvider`](https://developer.apple.com/documentation/networkextension/neappproxyprovider) or one of the other Network Extension entitlements, and those require special Apple approval. For typical foreground app use, structure your code to reconnect on `applicationWillEnterForeground` rather than expecting the socket to persist.

### Cancellation and timeouts

Two mechanisms bound how long an `Event` client waits: per-operation timeouts (built into every I/O method) and Swift Concurrency `Task.cancel()` from a parent scope. Use the first for tight per-call SLAs, the second for “give up the whole sequence.”

```swift
// Per-operation timeout (built into the API):
let response = try await socket.read(timeout: .seconds(5))

// Outer task cancellation (Swift structured concurrency):
let task = Task {
    try await sendCommand("PING", to: host, port: port)
}
// elsewhere:
task.cancel()
```

Per-operation timeouts surface as [`SocketError.timeout`](/documentation/Event/SocketError/timeout). Task cancellation propagates as `CancellationError` from any in-flight [`read(maxBytes:timeout:)`](/documentation/Event/Socket/read(maxBytes:timeout:)) / [`write(_:timeout:)`](/documentation/Event/Socket/write(_:timeout:)).

> Note: ``doc://Event/documentation/Event``’s cancellation does not currently unregister the outstanding libevent event from the ``doc://Event/documentation/Event/EventLoop`` until the next dispatch pass (see <doc://Event/documentation/Event/ProductionConsiderations>). For most app-level use cases this is invisible; for tight cancellation guarantees, prefer per-operation `timeout:` parameters over relying on `Task.cancel()`.

### Why not `NWConnection`?

`NWConnection` is the right answer when:

- You need TLS without bringing your own — `NWParameters.tls` is built in.
- You want multipath, low-data mode, or proxy-aware routing.
- You’re integrating with Network.framework’s path-monitoring APIs to react to interface changes.

[`Event`](/documentation/Event) is the right answer when:

- You want plain TCP without the callback-bridging boilerplate — ``doc://Event/documentation/Event/Socket/connect(to:port:loop:timeout:)`` returns a ready-to-use ``doc://Event/documentation/Event/Socket``.
- You need cross-platform reach to Linux (Network.framework is Apple-only).
- You want a small dependency footprint and a flat surface — no channel pipelines, no protocol framers, no `NWParameters` knob discovery.
- You’re embedding C code that already speaks libevent (the ``doc://Event/documentation/Event`` package ships the raw `libevent` C product alongside the Swift API — see <doc://Event/documentation/Event/EmbeddingCLibrariesInSwiftPM>).

For a pairwise comparison including SwiftNIO, Hummingbird, and Network.framework, see [swift-event vs SwiftNIO vs Hummingbird vs Network.framework](/documentation/Event/SwiftEventVsSwiftNIOVsHummingbirdVsNetworkFramework).

### A real example: a length-prefixed protocol client

Real-world TCP protocols frame their payloads — usually with a length prefix followed by exactly that many bytes. The `Event` snippet below shows the full pattern (reader, writer, and a sender/receiver demo running on two separate event loops):

```swift

```

> Important: TCP is a byte stream, not a message channel. **A single ``doc://Event/documentation/Event/Socket/read(maxBytes:timeout:)`` call returns whatever the kernel had ready** — it might be a partial message, a coalesced chunk of two messages, or a single byte. If your application protocol expects discrete messages, *you* have to add the framing. This is the most common newcomer-TCP bug across every language; <doc://Event/documentation/Event/PartialReadsAndMessageFraming> covers it in depth.

This pattern is generic enough to apply to any length-prefixed protocol (Redis RESP, custom binary formats, many game-server protocols). The deeper discussion of the alternate delimiter-framing pattern for line-oriented protocols (HTTP/1, SMTP, IRC) also lives in [Handling Partial Reads in Swift TCP Sockets: Length-Prefix and Delimiter Framing](/documentation/Event/PartialReadsAndMessageFraming).

## See Also

[Getting Started with Event in Swift](/documentation/Event/GettingStarted)

A task-oriented walkthrough of the three shipping capabilities in `Event`: inspecting the I/O backend, writing an async TCP client, and writing an async TCP server.

[How to Write an Async TCP Server in Swift](/documentation/Event/AsyncTCPServerInSwift)

A complete async TCP server in Swift 6, end-to-end — bind, listen, accept, per-connection handler tasks, graceful close — without channel pipelines or `EventLoopFuture` plumbing.

[Handling Partial Reads in Swift TCP Sockets: Length-Prefix and Delimiter Framing](/documentation/Event/PartialReadsAndMessageFraming)

Why a single `read()` call rarely returns a full message, and the two framing patterns (length-prefix and delimiter) every TCP application ends up writing.

[swift-event vs SwiftNIO vs Hummingbird vs Network.framework](/documentation/Event/SwiftEventVsSwiftNIOVsHummingbirdVsNetworkFramework)

A decision-tree comparison of the four common Swift networking choices, written from inside swift-event but trying not to mark its own homework.

[Production Considerations](/documentation/Event/ProductionConsiderations)

Pre-1.0 status, the concurrency model, resource-ownership rules, and the list of capabilities not yet shipping in `Event`.

[`Socket`](/documentation/Event/Socket)

An async non-blocking TCP socket backed by libevent.

[`connect(to:port:loop:timeout:)`](/documentation/Event/Socket/connect(to:port:loop:timeout:))

Connects to a remote IPv4 host by numeric address.

[`read(maxBytes:timeout:)`](/documentation/Event/Socket/read(maxBytes:timeout:))

Reads up to `maxBytes` bytes from the socket, awaiting readiness.

[`write(_:timeout:)`](/documentation/Event/Socket/write(_:timeout:))

Writes all bytes in `data` to the socket, awaiting write-readiness.

[`SocketError`](/documentation/Event/SocketError)

An error surfaced by [`SocketAddress`](/documentation/Event/SocketAddress), [`Socket`](/documentation/Event/Socket), and [`ServerSocket`](/documentation/Event/ServerSocket) operations.

[`SocketError.timeout`](/documentation/Event/SocketError/timeout)

A `timeout:` parameter elapsed before the operation completed.

[`SocketError.connectionClosed`](/documentation/Event/SocketError/connectionClosed)

The peer performed an orderly shutdown of its write side (`read(2)` returned 0).

