ios-networking
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseiOS 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 for large files -- it streams to disk instead of
loading the entire payload into memory.
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)对于大文件,使用方法——它会将数据流式写入磁盘,而非将整个负载加载到内存中。
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 for streaming responses, progress tracking, or
line-delimited data (e.g., server-sent events).
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)
}对于流式响应、进度跟踪或行分隔数据(如服务器发送事件),使用方法。
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 , optional custom , ,
and an array of interceptors. Each method builds a
from the endpoint, applies middleware, executes the request,
validates the status code, and decodes the result. See
for the complete implementation
with convenience methods, request builder, and test setup.
baseURLURLSessionJSONDecoderRequestMiddlewareURLRequestreferences/urlsession-patterns.mdAPIClient为了可测试性定义协议。这样在测试时可以直接替换实现,无需直接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!
}
}客户端接收、可选的自定义、以及拦截器数组。每个方法都会根据端点构建、应用中间件、执行请求、验证状态码并解码结果。完整的实现(包含便捷方法、请求构建器和测试设置)请参考。
baseURLURLSessionJSONDecoderRequestMiddlewareURLRequestAPIClientreferences/urlsession-patterns.mdLightweight Closure-Based Client
轻量级闭包式客户端
For apps using the MV pattern, use closure-based clients for testability
and SwiftUI preview support. See for
the full pattern (struct of async closures, injected via init).
references/lightweight-clients.md对于使用MV模式的应用,使用闭包式客户端以提升可测试性和SwiftUI预览支持。完整模式(异步闭包结构体,通过初始化注入)请参考。
references/lightweight-clients.mdRequest 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 Code | Meaning | Action |
|---|---|---|
| Device offline | Show offline UI, queue for retry |
| Connection dropped mid-request | Retry with backoff |
| Server did not respond in time | Retry once, then show error |
| Task was cancelled | No action needed; do not show error |
| DNS failure | Check URL, show error |
| TLS handshake failed | Check cert pinning, ATS config |
| 401 from proxy | Trigger auth flow |
| URLError Code | Meaning | Action |
|---|---|---|
| 设备离线 | 显示离线UI,将请求加入队列等待重试 |
| 请求过程中连接中断 | 退避重试 |
| 服务器未及时响应 | 重试一次后显示错误 |
| 任务被取消 | 无需操作;不显示错误 |
| DNS解析失败 | 检查URL,显示错误 |
| TLS握手失败 | 检查证书绑定、ATS配置 |
| 代理返回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 .
Always check between pages. See
for complete and
offset-based implementations.
AsyncSequenceTask.isCancelledreferences/urlsession-patterns.mdCursorPaginator使用实现基于游标或偏移量的分页。在分页之间务必检查。完整的和基于偏移量的实现请参考。
AsyncSequenceTask.isCancelledCursorPaginatorreferences/urlsession-patterns.mdNetwork Reachability
网络可达性监控
Use from the Network framework — not third-party
Reachability libraries. Wrap in for structured concurrency.
NWPathMonitorAsyncStreamswift
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 (cellular) and (Low Data
Mode) to adapt behavior (reduce image quality, skip prefetching).
path.isExpensivepath.isConstrained使用Network框架中的——不要使用第三方Reachability库。将其包装在中以支持结构化并发。
NWPathMonitorAsyncStreamswift
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.isExpensivepath.isConstrainedConfiguring URLSession
配置URLSession
Create a configured session for production code. is
acceptable only for simple, one-off requests.
URLSession.sharedswift
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 = trueurlSession(_:taskIsWaitingForConnectivity:)为生产代码创建一个配置好的会话。仅在简单的一次性请求中使用。
URLSession.sharedswift
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 = trueurlSession(_:taskIsWaitingForConnectivity:)Common Mistakes
常见错误
DON'T: Use with custom configuration needs.
DO: Create a configured with appropriate timeouts, caching,
and delegate for production code.
URLSession.sharedURLSessionDON'T: Force-unwrap with dynamic input.
DO: Use with proper error handling. Force-unwrap is
acceptable only for compile-time-constant strings.
URL(string:)URL(string:)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 to update UI state.
@MainActorDON'T: Ignore cancellation in long-running network tasks.
DO: Check or call in
loops (pagination, streaming, retry). Use in SwiftUI for automatic
cancellation.
Task.isCancelledtry Task.checkCancellation().taskDON'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 subclass for transport-level mocking, or use
protocol-based clients that accept a test double.
URLProtocolDON'T: Use for large file downloads.
DO: Use which streams to disk and avoids memory spikes.
data(for:)download(for:)DON'T: Fire network requests from or view initializers.
DO: Use or to trigger network calls.
body.task.task(id:)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.sharedURLSession错误做法: 对动态输入的进行强制解包。
正确做法: 对进行适当的错误处理。仅对编译时常量字符串进行强制解包是可接受的。
URL(string:)URL(string:)错误做法: 在主线程上解码大型JSON负载。
正确做法: 保持解码操作在URLSession调用的上下文(默认非主线程)中执行。仅在需要更新UI时切换到。
@MainActor错误做法: 在长时间运行的网络任务中忽略取消操作。
正确做法: 在循环(分页、流式处理、重试)中检查或调用。在SwiftUI中使用实现自动取消。
Task.isCancelledtry Task.checkCancellation().task错误做法: 在URLSession async/await可满足需求时使用Alamofire或Moya。
正确做法: 直接使用URLSession。有了async/await后,第三方库的易用性优势已不复存在。仅在确实需要缺失的功能(如图像缓存)时才使用第三方库。
错误做法: 在测试中直接Mock URLSession。
正确做法: 使用子类进行传输层Mock,或使用接受测试替身的基于协议的客户端。
URLProtocol错误做法: 使用下载大文件。
正确做法: 使用,它会流式写入磁盘,避免内存峰值。
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 modifier or stored Task references)
.task - Authentication tokens injected via middleware, not hardcoded
- Response HTTP status codes validated before decoding
- Large downloads use not
download(for:)data(for:) - Network calls happen off (only UI updates on main)
@MainActor - URLSession configured with appropriate timeouts and caching
- Retry logic excludes cancellation and 4xx client errors
- Pagination checks between pages
Task.isCancelled - 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:) - 网络调用在之外执行(仅UI更新在主线程)
@MainActor - URLSession配置了适当的超时和缓存策略
- 重试逻辑排除了取消和4xx客户端错误
- 分页时在页面之间检查
Task.isCancelled - 敏感Token存储在Keychain中(而非UserDefaults或明文文件)
- 不对动态输入的URL进行强制解包
- 服务器错误响应被解码并展示给用户
- 确保网络响应模型类型符合Sendable;使用@MainActor处理更新UI的完成路径
Reference Material
参考资料
- See for complete API client implementation, multipart uploads, download progress, URLProtocol mocking, retry/backoff, certificate pinning, request logging, and pagination implementations.
references/urlsession-patterns.md - See for background URLSession configuration, background downloads/uploads, WebSocket patterns with structured concurrency, and reconnection strategies.
references/background-websocket.md - See for the lightweight closure-based client pattern (struct of async closures, injected via init for testability and preview support).
references/lightweight-clients.md
- 完整的API客户端实现、多部分上传、下载进度、URLProtocol Mock、重试/退避、证书绑定、请求日志和分页实现,请参考。
references/urlsession-patterns.md - 后台URLSession配置、后台下载/上传、结合结构化并发的WebSocket模式以及重连策略,请参考。
references/background-websocket.md - 轻量级闭包式客户端模式(异步闭包结构体,通过初始化注入以支持可测试性和预览),请参考。
references/lightweight-clients.md