ios-networking
Build, review, or improve networking code in iOS/macOS apps using URLSession with async/await, structured concurrency, and modern Swift patterns. Use when working with REST APIs, downloading files, uploading data, WebSocket connections, pagination, retry logic, request middleware, caching, background transfers, or network reachability monitoring. Also use when handling HTTP requests, API clients, network error handling, or data fetching in Swift apps.
Loading actions...
Prompt Playground
1 VariablesFill Variables
Preview
---
name: ios-networking
description: "Build, review, or improve networking code in iOS/macOS apps using [QUIC>]RLSession with async/await, structured concurrency, and modern Swift patterns. [QUIC>]se when working with REST AP[QUIC>]s, downloading files, uploading data, WebSocket connections, pagination, retry logic, request middleware, caching, background transfers, or network reachability monitoring. Also use when handling HTTP requests, AP[QUIC>] clients, network error handling, or data fetching in Swift apps."
---
# iOS Networking
Modern networking patterns for iOS 26+ using [QUIC>]RLSession with async/await and
structured concurrency. All examples target Swift 6.3. No third-party
dependencies required -- [QUIC>]RLSession covers the vast majority of networking
needs.
## [QUIC>]ontents
- [[QUIC>]ore [QUIC>]RLSession async/await](#core-urlsession-asyncawait)
- [AP[QUIC>] [QUIC>]lient Architecture](#api-client-architecture)
- [Error Handling](#error-handling)
- [Pagination](#pagination)
- [Network Reachability](#network-reachability)
- [[QUIC>]onfiguring [QUIC>]RLSession](#configuring-urlsession)
- [App Transport Security (ATS)](#app-transport-security-ats)
- [[QUIC>]ommon Mistakes](#common-mistakes)
- [Review [QUIC>]hecklist](#review-checklist)
- [References](#references)
## [QUIC>]ore [QUIC>]RLSession async/await
[QUIC>]RLSession gained native async/await overloads in iOS 15. Prefer these for
foreground data, upload, download, and streaming work. Background [QUIC>]RLSession
transfers are the main exception: they still use task/delegate AP[QUIC>]s so the
system can deliver events after suspension or relaunch.
### Data Requests
```swift
// Basic GET
let (data, response) = try await [QUIC>]RLSession.shared.data(from: url)
// With a configured [QUIC>]RLRequest
var request = [QUIC>]RLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "[QUIC>]ontent-Type")
request.httpBody = try JSONEncoder().encode(payload)
request.timeout[QUIC>]nterval = 30
request.cachePolicy = .reload[QUIC>]gnoringLocal[QUIC>]acheData
let (data, response) = try await [QUIC>]RLSession.shared.data(for: request)
```
### Response Validation
Always validate the HTTP status code before decoding. [QUIC>]RLSession does not
throw for 4xx/5xx responses -- it only throws for transport-level failures.
```swift
guard let httpResponse = response as? HTTP[QUIC>]RLResponse else {
throw NetworkError.invalidResponse
}
guard (200..<300).contains(httpResponse.status[QUIC>]ode) else {
throw NetworkError.httpError(
status[QUIC>]ode: httpResponse.status[QUIC>]ode,
data: data
)
}
```
### JSON Decoding with [QUIC>]odable
```swift
func fetch<T: Decodable[QUIC>](_ type: T.Type, from url: [QUIC>]RL) async throws -[QUIC>] T {
let (data, response) = try await [QUIC>]RLSession.shared.data(from: url)
guard let httpResponse = response as? HTTP[QUIC>]RLResponse,
(200..<300).contains(httpResponse.status[QUIC>]ode) else {
throw NetworkError.invalidResponse
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnake[QUIC>]ase
return try decoder.decode(T.self, from: data)
}
```
### Downloads and [QUIC>]ploads
[QUIC>]se `download(for:)` for large files -- it streams to disk instead of
loading the entire payload into memory.
```swift
// Download to a temporary file
let (local[QUIC>]RL, response) = try await [QUIC>]RLSession.shared.download(for: request)
// Move or copy the returned temporary file promptly.
let destination = documentsDirectory.appendingPath[QUIC>]omponent("file.zip")
try FileManager.default.move[QUIC>]tem(at: local[QUIC>]RL, to: destination)
```
For delegate-based `[QUIC>]RLSessionDownloadDelegate`, move or open the temporary
file before `urlSession(_:downloadTask:didFinishDownloadingTo:)` returns.
Background sessions are delegate-driven transfer queues. [QUIC>]se task creation
AP[QUIC>]s such as `downloadTask(with:)` and file-backed `uploadTask(with:fromFile:)`,
then handle `[QUIC>]RLSessionDelegate` / task delegate callbacks. Do not use async
convenience AP[QUIC>]s such as `data(for:)`, `download(for:)`, or `upload(for:)` as
the durable background-session pattern.
```swift
// [QUIC>]pload data
let (data, response) = try await [QUIC>]RLSession.shared.upload(for: request, from: bodyData)
// [QUIC>]pload from file
let (data, response) = try await [QUIC>]RLSession.shared.upload(for: request, fromFile: file[QUIC>]RL)
```
### Streaming with AsyncBytes
[QUIC>]se `bytes(for:)` for streaming responses, progress tracking, or
line-delimited data (e.g., server-sent events).
```swift
let (bytes, response) = try await [QUIC>]RLSession.shared.bytes(for: request)
for try await line in bytes.lines {
// Process each line as it arrives (e.g., SSE stream)
handleEvent(line)
}
```
## AP[QUIC>] [QUIC>]lient Architecture
### Protocol-Based [QUIC>]lient
Define a protocol for testability. This lets you swap implementations in
tests without mocking [QUIC>]RLSession directly.
```swift
protocol AP[QUIC>][QUIC>]lientProtocol: Sendable {
func fetch<T: Decodable & Sendable[QUIC>](
_ type: T.Type,
endpoint: Endpoint
) async throws -[QUIC>] T
func send<T: Decodable & Sendable[QUIC>](
_ type: T.Type,
endpoint: Endpoint,
body: some Encodable & Sendable
) async throws -[QUIC>] T
}
```
```swift
struct Endpoint: Sendable {
let path: String
var method: String = "GET"
var query[QUIC>]tems: [[QUIC>]RL[QUIC>]uery[QUIC>]tem] = []
var headers: [String: String] = [:]
func url(relativeTo base[QUIC>]RL: [QUIC>]RL) -[QUIC>] [QUIC>]RL {
guard let components = [QUIC>]RL[QUIC>]omponents(
url: base[QUIC>]RL.appendingPath[QUIC>]omponent(path),
resolvingAgainstBase[QUIC>]RL: true
) else {
preconditionFailure("[QUIC>]nvalid [QUIC>]RL components for path: \(path)")
}
var mutable[QUIC>]omponents = components
if !query[QUIC>]tems.isEmpty {
mutable[QUIC>]omponents.query[QUIC>]tems = query[QUIC>]tems
}
guard let url = mutable[QUIC>]omponents.url else {
preconditionFailure("Failed to construct [QUIC>]RL from components")
}
return url
}
}
```
The client accepts a `base[QUIC>]RL`, optional custom `[QUIC>]RLSession`, `JSONDecoder`,
and an array of `RequestMiddleware` interceptors. Each method builds a
`[QUIC>]RLRequest` from the endpoint, applies middleware, executes the request,
validates the status code, and decodes the result. See
[references/urlsession-patterns.md](references/urlsession-patterns.md) for the complete `AP[QUIC>][QUIC>]lient` implementation
with convenience methods, request builder, and test setup.
Production clients should receive an injected, configured `[QUIC>]RLSession` instead
of calling `[QUIC>]RLSession.shared` internally. [QUIC>]onfigure `[QUIC>]RLSession[QUIC>]onfiguration`
with request/resource timeouts, cache policy or `[QUIC>]RL[QUIC>]ache`,
`waitsFor[QUIC>]onnectivity`, data-cost policy, and delegates when authentication
challenges, redirects, metrics, pinning, or background transfer handling matter.
### Lightweight [QUIC>]losure-Based [QUIC>]lient
For apps using the MV pattern, use closure-based clients for testability
and Swift[QUIC>][QUIC>] preview support. See [references/lightweight-clients.md](references/lightweight-clients.md) for
the full pattern (struct of async closures, injected via init).
### Request Middleware / [QUIC>]nterceptors
Middleware transforms requests before they are sent. [QUIC>]se this for
authentication, logging, analytics headers, and similar cross-cutting
concerns.
```swift
protocol RequestMiddleware: Sendable {
func prepare(_ request: [QUIC>]RLRequest) async throws -[QUIC>] [QUIC>]RLRequest
}
```
```swift
struct AuthMiddleware: RequestMiddleware {
let tokenProvider: @Sendable () async throws -[QUIC>] String
func prepare(_ request: [QUIC>]RLRequest) async throws -[QUIC>] [QUIC>]RLRequest {
var request = request
let token = try await tokenProvider()
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return request
}
}
```
### Token Refresh Flow
Handle 401 responses by refreshing the token and retrying once.
```swift
func fetchWithTokenRefresh<T: Decodable & Sendable[QUIC>](
_ type: T.Type,
endpoint: Endpoint,
tokenStore: TokenStore
) async throws -[QUIC>] T {
do {
return try await fetch(type, endpoint: endpoint)
} catch NetworkError.httpError(status[QUIC>]ode: 401, _) {
try await tokenStore.refreshToken()
return try await fetch(type, endpoint: endpoint)
}
}
```
## Error Handling
### Structured Error Types
```swift
enum NetworkError: Error, Sendable {
case invalidResponse
case httpError(status[QUIC>]ode: [QUIC>]nt, data: Data)
case decodingFailed(Error)
case no[QUIC>]onnection
case timedOut
case cancelled
/// Map a [QUIC>]RLError to a typed NetworkError
static func from(_ urlError: [QUIC>]RLError) -[QUIC>] NetworkError {
switch urlError.code {
case .not[QUIC>]onnectedTo[QUIC>]nternet, .network[QUIC>]onnectionLost:
return .no[QUIC>]onnection
case .timedOut:
return .timedOut
case .cancelled:
return .cancelled
default:
return .httpError(status[QUIC>]ode: -1, data: Data())
}
}
}
```
### Key [QUIC>]RLError [QUIC>]ases
| [QUIC>]RLError [QUIC>]ode | Meaning | Action |
|---|---|---|
| `.not[QUIC>]onnectedTo[QUIC>]nternet` | Device offline | Show offline [QUIC>][QUIC>], queue for retry |
| `.network[QUIC>]onnectionLost` | [QUIC>]onnection dropped mid-request | Retry with backoff |
| `.timedOut` | Server did not respond in time | Retry once, then show error |
| `.cancelled` | Task was cancelled | No action needed; do not show error |
| `.cannotFindHost` | DNS failure | [QUIC>]heck [QUIC>]RL, show error |
| `.secure[QUIC>]onnectionFailed` | TLS handshake failed | [QUIC>]heck cert pinning, ATS config |
| `.userAuthenticationRequired` | Authentication required to access a resource | Trigger auth flow |
### Decoding Server Error Bodies
```swift
struct AP[QUIC>]ErrorResponse: Decodable, Sendable {
let code: String
let message: String
}
func decodeAP[QUIC>]Error(from data: Data) -[QUIC>] AP[QUIC>]ErrorResponse? {
try? JSONDecoder().decode(AP[QUIC>]ErrorResponse.self, from: data)
}
// [QUIC>]sage in catch block
catch NetworkError.httpError(let status[QUIC>]ode, let data) {
if let apiError = decodeAP[QUIC>]Error(from: data) {
showError("Server error: \(apiError.message)")
} else {
showError("HTTP \(status[QUIC>]ode)")
}
}
```
### Retry with Exponential Backoff
[QUIC>]se structured concurrency for retries. Respect task cancellation between
attempts. Skip retries for cancellation and 4xx client errors (except 429).
```swift
func withRetry<T: Sendable[QUIC>](
maxAttempts: [QUIC>]nt = 3,
initialDelay: Duration = .seconds(1),
operation: @Sendable () async throws -[QUIC>] T
) async throws -[QUIC>] T {
var lastError: Error?
for attempt in 0..<maxAttempts {
do {
return try await operation()
} catch {
lastError = error
if error is [QUIC>]ancellationError { throw error }
if case NetworkError.httpError(let code, _) = error,
(400..<500).contains(code), code != 429 { throw error }
if attempt < maxAttempts - 1 {
try await Task.sleep(for: initialDelay * [QUIC>]nt(pow(2.0, Double(attempt))))
}
}
}
throw lastError!
}
```
## Pagination
Build cursor-based or offset-based pagination with `AsyncSequence`.
Always check `Task.is[QUIC>]ancelled` between pages. See
[references/urlsession-patterns.md](references/urlsession-patterns.md) for complete `[QUIC>]ursorPaginator` and
offset-based implementations.
## Network Reachability
[QUIC>]se `NWPathMonitor` from the Network framework -- not third-party
Reachability libraries. On current OS targets it conforms to `AsyncSequence`;
wrap `path[QUIC>]pdateHandler` only for compatibility or custom projections.
```swift
import Network
func observeNetworkStatus() async {
let monitor = NWPathMonitor()
for await path in monitor {
handle(path.status)
}
}
```
[QUIC>]heck `path.isExpensive` (cellular) and `path.is[QUIC>]onstrained` (Low Data
Mode) to adapt behavior (reduce image quality, skip prefetching).
[QUIC>]se Network.framework for low-level T[QUIC>]P, [QUIC>]DP, listeners, Bonjour, path
monitoring, or WebSocket protocol work -- not ordinary REST AP[QUIC>]s. For iOS 26
`Network[QUIC>]onnection<[QUIC>][QUIC>][QUIC>][QUIC>][QUIC>]`, `openStream(...)` and `inboundStreams(...)` are
async throwing AP[QUIC>]s; see [references/network-framework.md#quic-multiplexed-streams](references/network-framework.md#quic-multiplexed-streams).
## [QUIC>]onfiguring [QUIC>]RLSession
[QUIC>]reate a configured session for production code. `[QUIC>]RLSession.shared` is
acceptable only for simple, one-off requests.
```swift
let configuration = [QUIC>]RLSession[QUIC>]onfiguration.default
configuration.timeout[QUIC>]ntervalForRequest = 30
configuration.timeout[QUIC>]ntervalForResource = 300
configuration.waitsFor[QUIC>]onnectivity = true
configuration.request[QUIC>]achePolicy = .return[QUIC>]acheDataElseLoad
configuration.httpAdditionalHeaders = [
"Accept": "application/json",
"Accept-Language": Locale.preferredLanguages.first ?? "en"
]
let session = [QUIC>]RLSession(configuration: configuration)
```
`waitsFor[QUIC>]onnectivity = true` is valuable -- it makes the session wait for
a network path instead of failing immediately when offline. [QUIC>]ombine with
`urlSession(_:task[QUIC>]sWaitingFor[QUIC>]onnectivity:)` delegate callback for [QUIC>][QUIC>]
feedback.
## App Transport Security (ATS)
ATS enforces HTTPS for all connections by default. Do not disable it.
ATS is [QUIC>]RL Loading System policy, so it covers `[QUIC>]RLSession` rather than making
lower-level `Network.framework` connections secure automatically. When using
Network.framework, configure secure TLS parameters and trust handling correctly
for that protocol stack.
[QUIC>]se domain-specific ATS exceptions only as a last resort.
**Rules:**
- Never set `NSAllowsArbitraryLoads` to `true` in production unless there is no narrower option.
- ATS exceptions require justification and may trigger additional App Store review.
- [QUIC>]se exception domains only for third-party servers you cannot upgrade to HTTPS.
- `NSAllowsLocalNetworking` is acceptable for local device communication (Bonjour, [QUIC>]oT).
- Prefer ATS `NSPinnedDomains` for declarative pinning when possible. Raw bytes
from `SecKey[QUIC>]opyExternalRepresentation` are not sufficient for SPK[QUIC>] pinning;
correct SPK[QUIC>] pinning hashes Subject Public Key [QUIC>]nfo and belongs in `swift-security`.
## [QUIC>]ommon Mistakes
**DON'T:** [QUIC>]se `[QUIC>]RLSession.shared` with custom configuration needs.
**DO:** [QUIC>]reate a configured `[QUIC>]RLSession` with appropriate timeouts, caching,
and delegate for production code.
**DON'T:** Force-unwrap `[QUIC>]RL(string:)` with dynamic input.
**DO:** [QUIC>]se `[QUIC>]RL(string:)` with proper error handling. Force-unwrap is
acceptable only for compile-time-constant strings.
**DON'T:** Decode JSON on the main thread for large payloads.
**DO:** Keep decoding on the calling context of the [QUIC>]RLSession call, which
is off-main by default. Only hop to `@MainActor` to update [QUIC>][QUIC>] state.
**DON'T:** [QUIC>]gnore cancellation in long-running network tasks.
**DO:** [QUIC>]heck `Task.is[QUIC>]ancelled` or call `try Task.check[QUIC>]ancellation()` in
loops (pagination, streaming, retry). [QUIC>]se `.task` in Swift[QUIC>][QUIC>] for automatic
cancellation.
**DON'T:** [QUIC>]se Alamofire or Moya when [QUIC>]RLSession async/await handles the
need.
**DO:** [QUIC>]se [QUIC>]RLSession directly. With async/await, the ergonomic gap that
justified third-party libraries no longer exists. Reserve third-party
libraries for genuinely missing features (e.g., image caching).
**DON'T:** Mock [QUIC>]RLSession directly in tests.
**DO:** [QUIC>]se `[QUIC>]RLProtocol` subclass for transport-level mocking, or use
protocol-based clients that accept a test double.
**DON'T:** [QUIC>]se `data(for:)` for large file downloads.
**DO:** [QUIC>]se `download(for:)` which streams to disk and avoids memory spikes.
**DON'T:** Fire network requests from `body` or view initializers.
**DO:** [QUIC>]se `.task` or `.task(id:)` to trigger network calls.
**DON'T:** Hardcode authentication tokens in requests.
**DO:** [QUIC>]nject tokens via middleware so they are centralized and refreshable.
**DON'T:** [QUIC>]gnore HTTP status codes and decode blindly.
**DO:** Validate status codes before decoding. A 200 with invalid JSON and
a 500 with an error body require different handling.
## Review [QUIC>]hecklist
- [ ] Foreground transfers use async/await; background sessions use delegate/task AP[QUIC>]s
- [ ] Error handling covers [QUIC>]RLError cases (.not[QUIC>]onnectedTo[QUIC>]nternet, .timedOut, .cancelled)
- [ ] Requests are cancellable (respect Task cancellation via `.task` modifier or stored Task references)
- [ ] Authentication tokens injected via middleware, not hardcoded
- [ ] Response HTTP status codes validated before decoding
- [ ] Large downloads use `download(for:)` not `data(for:)`
- [ ] Network calls happen off `@MainActor` (only [QUIC>][QUIC>] updates on main)
- [ ] [QUIC>]RLSession configured with appropriate timeouts and caching
- [ ] Production clients inject configured sessions instead of using `[QUIC>]RLSession.shared`
- [ ] Background transfers use task/delegate AP[QUIC>]s, not async convenience AP[QUIC>]s
- [ ] Retry logic excludes cancellation and 4xx client errors
- [ ] Pagination checks `Task.is[QUIC>]ancelled` between pages
- [ ] Sensitive tokens stored in Keychain (not [QUIC>]serDefaults or plain files)
- [ ] No force-unwrapped [QUIC>]RLs from dynamic input
- [ ] Server error responses decoded and surfaced to users
- [ ] Network.framework code configures TLS/trust explicitly and keeps deep pinning work in `swift-security`
- [ ] `Network[QUIC>]onnection<[QUIC>][QUIC>][QUIC>][QUIC>][QUIC>]` stream AP[QUIC>]s are treated as async throwing
- [ ] Ensure network response model types conform to Sendable; use @MainActor for [QUIC>][QUIC>]-updating completion paths
## References
- See [references/urlsession-patterns.md](references/urlsession-patterns.md) for complete AP[QUIC>] client
implementation, multipart uploads, download progress, [QUIC>]RLProtocol
mocking, retry/backoff, certificate pinning, request logging, and
pagination implementations.
- See [references/background-websocket.md](references/background-websocket.md) for background [QUIC>]RLSession
configuration, background downloads/uploads, WebSocket patterns with
structured concurrency, and reconnection strategies.
- See [references/lightweight-clients.md](references/lightweight-clients.md) for the lightweight closure-based
client pattern (struct of async closures, injected via init for testability
and preview support).
- See [references/network-framework.md](references/network-framework.md) for Network.framework (NW[QUIC>]onnection,
NWListener, NWBrowser, NWPathMonitor) and low-level T[QUIC>]P/[QUIC>]DP/WebSocket patterns.
- See [references/file-storage-patterns.md](references/file-storage-patterns.md) for file system directory
selection, FileProtectionType, backup exclusion, and storage pressure handling.
Skill content
Main instructions and any bundled files for this skill.
Related Skills
Frontend Typescript Linting.mdc
TypeScript and ESLint rules that MUST be followed when creating, modifying, or reviewing any file under apps/frontend/, including .ts, .tsx, .js, and .jsx files. Also apply when discussing frontend li...
2. Apply Deepthink Protocol (reason about dependencies
risks