ios-networking

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

iOS Networking

iOS 网络编程

Modern networking patterns for iOS 26+ using URLSession with async/await and structured concurrency. All examples target Swift 6.2. No third-party dependencies required -- URLSession covers the vast majority of networking needs.
适用于iOS 26+的现代网络编程模式,使用带有async/await和结构化并发的URLSession。所有示例基于Swift 6.2,无需第三方依赖——URLSession可满足绝大多数网络需求。

Core URLSession async/await

核心URLSession async/await用法

URLSession gained native async/await overloads in iOS 15. These are the only networking APIs to use in new code. Never use completion-handler variants in new projects.
URLSession在iOS 15中新增了原生async/await重载方法。在新项目中应只使用这些网络API,切勿使用基于完成回调的版本。

Data Requests

数据请求

swift
// Basic GET
let (data, response) = try await URLSession.shared.data(from: url)

// With a configured URLRequest
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(payload)
request.timeoutInterval = 30
request.cachePolicy = .reloadIgnoringLocalCacheData

let (data, response) = try await URLSession.shared.data(for: request)
swift
// Basic GET
let (data, response) = try await URLSession.shared.data(from: url)

// With a configured URLRequest
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(payload)
request.timeoutInterval = 30
request.cachePolicy = .reloadIgnoringLocalCacheData

let (data, response) = try await URLSession.shared.data(for: request)

Response Validation

响应验证

Always validate the HTTP status code before decoding. URLSession does not throw for 4xx/5xx responses -- it only throws for transport-level failures.
swift
guard let httpResponse = response as? HTTPURLResponse else {
    throw NetworkError.invalidResponse
}

guard (200..<300).contains(httpResponse.statusCode) else {
    throw NetworkError.httpError(
        statusCode: httpResponse.statusCode,
        data: data
    )
}
在解码前务必验证HTTP状态码。URLSession不会为4xx/5xx响应抛出异常——仅会在传输层面失败时抛出异常。
swift
guard let httpResponse = response as? HTTPURLResponse else {
    throw NetworkError.invalidResponse
}

guard (200..<300).contains(httpResponse.statusCode) else {
    throw NetworkError.httpError(
        statusCode: httpResponse.statusCode,
        data: data
    )
}

JSON Decoding with Codable

结合Codable的JSON解码

swift
func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          (200..<300).contains(httpResponse.statusCode) else {
        throw NetworkError.invalidResponse
    }

    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    return try decoder.decode(T.self, from: data)
}
swift
func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          (200..<300).contains(httpResponse.statusCode) else {
        throw NetworkError.invalidResponse
    }

    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    return try decoder.decode(T.self, from: data)
}

Downloads and Uploads

下载与上传

Use
download(for:)
for large files -- it streams to disk instead of loading the entire payload into memory.
swift
// Download to a temporary file
let (localURL, response) = try await URLSession.shared.download(for: request)

// Move from temp location before the method returns
let destination = documentsDirectory.appendingPathComponent("file.zip")
try FileManager.default.moveItem(at: localURL, to: destination)
swift
// Upload data
let (data, response) = try await URLSession.shared.upload(for: request, from: bodyData)

// Upload from file
let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
对于大文件,使用
download(for:)
方法——它会将数据流式写入磁盘,而非将整个负载加载到内存中。
swift
// Download to a temporary file
let (localURL, response) = try await URLSession.shared.download(for: request)

// Move from temp location before the method returns
let destination = documentsDirectory.appendingPathComponent("file.zip")
try FileManager.default.moveItem(at: localURL, to: destination)
swift
// Upload data
let (data, response) = try await URLSession.shared.upload(for: request, from: bodyData)

// Upload from file
let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)

Streaming with AsyncBytes

使用AsyncBytes流式处理

Use
bytes(for:)
for streaming responses, progress tracking, or line-delimited data (e.g., server-sent events).
swift
let (bytes, response) = try await URLSession.shared.bytes(for: request)

for try await line in bytes.lines {
    // Process each line as it arrives (e.g., SSE stream)
    handleEvent(line)
}
对于流式响应、进度跟踪或行分隔数据(如服务器发送事件),使用
bytes(for:)
方法。
swift
let (bytes, response) = try await URLSession.shared.bytes(for: request)

for try await line in bytes.lines {
    // Process each line as it arrives (e.g., SSE stream)
    handleEvent(line)
}

API Client Architecture

API客户端架构

Protocol-Based Client

基于协议的客户端

Define a protocol for testability. This lets you swap implementations in tests without mocking URLSession directly.
swift
protocol APIClientProtocol: Sendable {
    func fetch<T: Decodable & Sendable>(
        _ type: T.Type,
        endpoint: Endpoint
    ) async throws -> T

    func send<T: Decodable & Sendable>(
        _ type: T.Type,
        endpoint: Endpoint,
        body: some Encodable & Sendable
    ) async throws -> T
}
swift
struct Endpoint: Sendable {
    let path: String
    var method: String = "GET"
    var queryItems: [URLQueryItem] = []
    var headers: [String: String] = [:]

    func url(relativeTo baseURL: URL) -> URL {
        var components = URLComponents(
            url: baseURL.appendingPathComponent(path),
            resolvingAgainstBaseURL: true
        )!
        if !queryItems.isEmpty {
            components.queryItems = queryItems
        }
        return components.url!
    }
}
The client accepts a
baseURL
, optional custom
URLSession
,
JSONDecoder
, and an array of
RequestMiddleware
interceptors. Each method builds a
URLRequest
from the endpoint, applies middleware, executes the request, validates the status code, and decodes the result. See
references/urlsession-patterns.md
for the complete
APIClient
implementation with convenience methods, request builder, and test setup.
为了可测试性定义协议。这样在测试时可以直接替换实现,无需直接Mock URLSession。
swift
protocol APIClientProtocol: Sendable {
    func fetch<T: Decodable & Sendable>(
        _ type: T.Type,
        endpoint: Endpoint
    ) async throws -> T

    func send<T: Decodable & Sendable>(
        _ type: T.Type,
        endpoint: Endpoint,
        body: some Encodable & Sendable
    ) async throws -> T
}
swift
struct Endpoint: Sendable {
    let path: String
    var method: String = "GET"
    var queryItems: [URLQueryItem] = []
    var headers: [String: String] = [:]

    func url(relativeTo baseURL: URL) -> URL {
        var components = URLComponents(
            url: baseURL.appendingPathComponent(path),
            resolvingAgainstBaseURL: true
        )!
        if !queryItems.isEmpty {
            components.queryItems = queryItems
        }
        return components.url!
    }
}
客户端接收
baseURL
、可选的自定义
URLSession
JSONDecoder
以及
RequestMiddleware
拦截器数组。每个方法都会根据端点构建
URLRequest
、应用中间件、执行请求、验证状态码并解码结果。完整的
APIClient
实现(包含便捷方法、请求构建器和测试设置)请参考
references/urlsession-patterns.md

Lightweight Closure-Based Client

轻量级闭包式客户端

For apps using the MV pattern, use closure-based clients for testability and SwiftUI preview support. See
references/lightweight-clients.md
for the full pattern (struct of async closures, injected via init).
对于使用MV模式的应用,使用闭包式客户端以提升可测试性和SwiftUI预览支持。完整模式(异步闭包结构体,通过初始化注入)请参考
references/lightweight-clients.md

Request Middleware / Interceptors

请求中间件/拦截器

Middleware transforms requests before they are sent. Use this for authentication, logging, analytics headers, and similar cross-cutting concerns.
swift
protocol RequestMiddleware: Sendable {
    func prepare(_ request: URLRequest) async throws -> URLRequest
}
swift
struct AuthMiddleware: RequestMiddleware {
    let tokenProvider: @Sendable () async throws -> String

    func prepare(_ request: URLRequest) async throws -> URLRequest {
        var request = request
        let token = try await tokenProvider()
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        return request
    }
}
中间件会在请求发送前对其进行转换。可用于身份验证、日志记录、分析头信息等横切关注点。
swift
protocol RequestMiddleware: Sendable {
    func prepare(_ request: URLRequest) async throws -> URLRequest
}
swift
struct AuthMiddleware: RequestMiddleware {
    let tokenProvider: @Sendable () async throws -> String

    func prepare(_ request: URLRequest) async throws -> URLRequest {
        var request = request
        let token = try await tokenProvider()
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        return request
    }
}

Token Refresh Flow

Token刷新流程

Handle 401 responses by refreshing the token and retrying once.
swift
func fetchWithTokenRefresh<T: Decodable & Sendable>(
    _ type: T.Type,
    endpoint: Endpoint,
    tokenStore: TokenStore
) async throws -> T {
    do {
        return try await fetch(type, endpoint: endpoint)
    } catch NetworkError.httpError(statusCode: 401, _) {
        try await tokenStore.refreshToken()
        return try await fetch(type, endpoint: endpoint)
    }
}
通过刷新Token并重试一次来处理401响应。
swift
func fetchWithTokenRefresh<T: Decodable & Sendable>(
    _ type: T.Type,
    endpoint: Endpoint,
    tokenStore: TokenStore
) async throws -> T {
    do {
        return try await fetch(type, endpoint: endpoint)
    } catch NetworkError.httpError(statusCode: 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(statusCode: Int, data: Data)
    case decodingFailed(Error)
    case noConnection
    case timedOut
    case cancelled

    /// Map a URLError to a typed NetworkError
    static func from(_ urlError: URLError) -> NetworkError {
        switch urlError.code {
        case .notConnectedToInternet, .networkConnectionLost:
            return .noConnection
        case .timedOut:
            return .timedOut
        case .cancelled:
            return .cancelled
        default:
            return .httpError(statusCode: -1, data: Data())
        }
    }
}
swift
enum NetworkError: Error, Sendable {
    case invalidResponse
    case httpError(statusCode: Int, data: Data)
    case decodingFailed(Error)
    case noConnection
    case timedOut
    case cancelled

    /// Map a URLError to a typed NetworkError
    static func from(_ urlError: URLError) -> NetworkError {
        switch urlError.code {
        case .notConnectedToInternet, .networkConnectionLost:
            return .noConnection
        case .timedOut:
            return .timedOut
        case .cancelled:
            return .cancelled
        default:
            return .httpError(statusCode: -1, data: Data())
        }
    }
}

Key URLError Cases

关键URLError场景

URLError CodeMeaningAction
.notConnectedToInternet
Device offlineShow offline UI, queue for retry
.networkConnectionLost
Connection dropped mid-requestRetry with backoff
.timedOut
Server did not respond in timeRetry once, then show error
.cancelled
Task was cancelledNo action needed; do not show error
.cannotFindHost
DNS failureCheck URL, show error
.secureConnectionFailed
TLS handshake failedCheck cert pinning, ATS config
.userAuthenticationRequired
401 from proxyTrigger auth flow
URLError CodeMeaningAction
.notConnectedToInternet
设备离线显示离线UI,将请求加入队列等待重试
.networkConnectionLost
请求过程中连接中断退避重试
.timedOut
服务器未及时响应重试一次后显示错误
.cancelled
任务被取消无需操作;不显示错误
.cannotFindHost
DNS解析失败检查URL,显示错误
.secureConnectionFailed
TLS握手失败检查证书绑定、ATS配置
.userAuthenticationRequired
代理返回401触发认证流程

Decoding Server Error Bodies

解码服务器错误响应体

swift
struct APIErrorResponse: Decodable, Sendable {
    let code: String
    let message: String
}

func decodeAPIError(from data: Data) -> APIErrorResponse? {
    try? JSONDecoder().decode(APIErrorResponse.self, from: data)
}

// Usage in catch block
catch NetworkError.httpError(let statusCode, let data) {
    if let apiError = decodeAPIError(from: data) {
        showError("Server error: \(apiError.message)")
    } else {
        showError("HTTP \(statusCode)")
    }
}
swift
struct APIErrorResponse: Decodable, Sendable {
    let code: String
    let message: String
}

func decodeAPIError(from data: Data) -> APIErrorResponse? {
    try? JSONDecoder().decode(APIErrorResponse.self, from: data)
}

// Usage in catch block
catch NetworkError.httpError(let statusCode, let data) {
    if let apiError = decodeAPIError(from: data) {
        showError("Server error: \(apiError.message)")
    } else {
        showError("HTTP \(statusCode)")
    }
}

Retry with Exponential Backoff

指数退避重试

Use structured concurrency for retries. Respect task cancellation between attempts. Skip retries for cancellation and 4xx client errors (except 429).
swift
func withRetry<T: Sendable>(
    maxAttempts: Int = 3,
    initialDelay: Duration = .seconds(1),
    operation: @Sendable () async throws -> T
) async throws -> T {
    var lastError: Error?
    for attempt in 0..<maxAttempts {
        do {
            return try await operation()
        } catch {
            lastError = error
            if error is CancellationError { 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 * Int(pow(2.0, Double(attempt))))
            }
        }
    }
    throw lastError!
}
使用结构化并发实现重试逻辑。在尝试之间要响应任务取消。对于取消和4xx客户端错误(429除外)不进行重试。
swift
func withRetry<T: Sendable>(
    maxAttempts: Int = 3,
    initialDelay: Duration = .seconds(1),
    operation: @Sendable () async throws -> T
) async throws -> T {
    var lastError: Error?
    for attempt in 0..<maxAttempts {
        do {
            return try await operation()
        } catch {
            lastError = error
            if error is CancellationError { 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 * Int(pow(2.0, Double(attempt))))
            }
        }
    }
    throw lastError!
}

Pagination

分页

Build cursor-based or offset-based pagination with
AsyncSequence
. Always check
Task.isCancelled
between pages. See
references/urlsession-patterns.md
for complete
CursorPaginator
and offset-based implementations.
使用
AsyncSequence
实现基于游标或偏移量的分页。在分页之间务必检查
Task.isCancelled
。完整的
CursorPaginator
和基于偏移量的实现请参考
references/urlsession-patterns.md

Network Reachability

网络可达性监控

Use
NWPathMonitor
from the Network framework — not third-party Reachability libraries. Wrap in
AsyncStream
for structured concurrency.
swift
import Network

func networkStatusStream() -> AsyncStream<NWPath.Status> {
    AsyncStream { continuation in
        let monitor = NWPathMonitor()
        monitor.pathUpdateHandler = { continuation.yield($0.status) }
        continuation.onTermination = { _ in monitor.cancel() }
        monitor.start(queue: DispatchQueue(label: "NetworkMonitor"))
    }
}
Check
path.isExpensive
(cellular) and
path.isConstrained
(Low Data Mode) to adapt behavior (reduce image quality, skip prefetching).
使用Network框架中的
NWPathMonitor
——不要使用第三方Reachability库。将其包装在
AsyncStream
中以支持结构化并发。
swift
import Network

func networkStatusStream() -> AsyncStream<NWPath.Status> {
    AsyncStream { continuation in
        let monitor = NWPathMonitor()
        monitor.pathUpdateHandler = { continuation.yield($0.status) }
        continuation.onTermination = { _ in monitor.cancel() }
        monitor.start(queue: DispatchQueue(label: "NetworkMonitor"))
    }
}
检查
path.isExpensive
(蜂窝网络)和
path.isConstrained
(低数据模式)来调整行为(降低图片质量、跳过预加载)。

Configuring URLSession

配置URLSession

Create a configured session for production code.
URLSession.shared
is acceptable only for simple, one-off requests.
swift
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 300
configuration.waitsForConnectivity = true
configuration.requestCachePolicy = .returnCacheDataElseLoad
configuration.httpAdditionalHeaders = [
    "Accept": "application/json",
    "Accept-Language": Locale.preferredLanguages.first ?? "en"
]

let session = URLSession(configuration: configuration)
waitsForConnectivity = true
is valuable -- it makes the session wait for a network path instead of failing immediately when offline. Combine with
urlSession(_:taskIsWaitingForConnectivity:)
delegate callback for UI feedback.
为生产代码创建一个配置好的会话。仅在简单的一次性请求中使用
URLSession.shared
swift
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 300
configuration.waitsForConnectivity = true
configuration.requestCachePolicy = .returnCacheDataElseLoad
configuration.httpAdditionalHeaders = [
    "Accept": "application/json",
    "Accept-Language": Locale.preferredLanguages.first ?? "en"
]

let session = URLSession(configuration: configuration)
waitsForConnectivity = true
非常实用——它会让会话等待网络连接,而不是在离线时立即失败。结合
urlSession(_:taskIsWaitingForConnectivity:)
代理回调提供UI反馈。

Common Mistakes

常见错误

DON'T: Use
URLSession.shared
with custom configuration needs. DO: Create a configured
URLSession
with appropriate timeouts, caching, and delegate for production code.
DON'T: Force-unwrap
URL(string:)
with dynamic input. DO: Use
URL(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 URLSession call, which is off-main by default. Only hop to
@MainActor
to update UI state.
DON'T: Ignore cancellation in long-running network tasks. DO: Check
Task.isCancelled
or call
try Task.checkCancellation()
in loops (pagination, streaming, retry). Use
.task
in SwiftUI for automatic cancellation.
DON'T: Use Alamofire or Moya when URLSession async/await handles the need. DO: Use URLSession 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 URLSession directly in tests. DO: Use
URLProtocol
subclass for transport-level mocking, or use protocol-based clients that accept a test double.
DON'T: Use
data(for:)
for large file downloads. DO: Use
download(for:)
which streams to disk and avoids memory spikes.
DON'T: Fire network requests from
body
or view initializers. DO: Use
.task
or
.task(id:)
to trigger network calls.
DON'T: Hardcode authentication tokens in requests. DO: Inject tokens via middleware so they are centralized and refreshable.
DON'T: Ignore 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.
错误做法: 在有自定义配置需求时使用
URLSession.shared
正确做法: 为生产代码创建一个配置了适当超时、缓存和代理的
URLSession
错误做法: 对动态输入的
URL(string:)
进行强制解包。 正确做法:
URL(string:)
进行适当的错误处理。仅对编译时常量字符串进行强制解包是可接受的。
错误做法: 在主线程上解码大型JSON负载。 正确做法: 保持解码操作在URLSession调用的上下文(默认非主线程)中执行。仅在需要更新UI时切换到
@MainActor
错误做法: 在长时间运行的网络任务中忽略取消操作。 正确做法: 在循环(分页、流式处理、重试)中检查
Task.isCancelled
或调用
try Task.checkCancellation()
。在SwiftUI中使用
.task
实现自动取消。
错误做法: 在URLSession async/await可满足需求时使用Alamofire或Moya。 正确做法: 直接使用URLSession。有了async/await后,第三方库的易用性优势已不复存在。仅在确实需要缺失的功能(如图像缓存)时才使用第三方库。
错误做法: 在测试中直接Mock URLSession。 正确做法: 使用
URLProtocol
子类进行传输层Mock,或使用接受测试替身的基于协议的客户端。
错误做法: 使用
data(for:)
下载大文件。 正确做法: 使用
download(for:)
,它会流式写入磁盘,避免内存峰值。
错误做法:
body
或视图初始化器中发起网络请求。 正确做法: 使用
.task
.task(id:)
触发网络调用。
错误做法: 在请求中硬编码认证Token。 正确做法: 通过中间件注入Token,使其集中管理且可刷新。
错误做法: 忽略HTTP状态码直接解码。 正确做法: 在解码前验证状态码。返回200但JSON无效,和返回500且带有错误体的情况需要不同的处理方式。

Review Checklist

审查清单

  • All network calls use async/await (not completion handlers)
  • Error handling covers URLError cases (.notConnectedToInternet, .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 UI updates on main)
  • URLSession configured with appropriate timeouts and caching
  • Retry logic excludes cancellation and 4xx client errors
  • Pagination checks
    Task.isCancelled
    between pages
  • Sensitive tokens stored in Keychain (not UserDefaults or plain files)
  • No force-unwrapped URLs from dynamic input
  • Server error responses decoded and surfaced to users
  • Ensure network response model types conform to Sendable; use @MainActor for UI-updating completion paths
  • 所有网络调用均使用async/await(而非完成回调)
  • 错误处理覆盖了URLError场景(.notConnectedToInternet、.timedOut、.cancelled)
  • 请求可取消(通过
    .task
    修饰符或存储的Task引用响应任务取消)
  • 认证Token通过中间件注入,而非硬编码
  • 在解码前验证响应HTTP状态码
  • 大文件下载使用
    download(for:)
    而非
    data(for:)
  • 网络调用在
    @MainActor
    之外执行(仅UI更新在主线程)
  • URLSession配置了适当的超时和缓存策略
  • 重试逻辑排除了取消和4xx客户端错误
  • 分页时在页面之间检查
    Task.isCancelled
  • 敏感Token存储在Keychain中(而非UserDefaults或明文文件)
  • 不对动态输入的URL进行强制解包
  • 服务器错误响应被解码并展示给用户
  • 确保网络响应模型类型符合Sendable;使用@MainActor处理更新UI的完成路径

Reference Material

参考资料

  • See
    references/urlsession-patterns.md
    for complete API client implementation, multipart uploads, download progress, URLProtocol mocking, retry/backoff, certificate pinning, request logging, and pagination implementations.
  • See
    references/background-websocket.md
    for background URLSession configuration, background downloads/uploads, WebSocket patterns with structured concurrency, and reconnection strategies.
  • See
    references/lightweight-clients.md
    for the lightweight closure-based client pattern (struct of async closures, injected via init for testability and preview support).
  • 完整的API客户端实现、多部分上传、下载进度、URLProtocol Mock、重试/退避、证书绑定、请求日志和分页实现,请参考
    references/urlsession-patterns.md
  • 后台URLSession配置、后台下载/上传、结合结构化并发的WebSocket模式以及重连策略,请参考
    references/background-websocket.md
  • 轻量级闭包式客户端模式(异步闭包结构体,通过初始化注入以支持可测试性和预览),请参考
    references/lightweight-clients.md