使用 async/await 和结构化并发,在 iOS/macOS 应用中构建、审查或改进基于 URLSession 的网络代码。适用于 REST API、文件下载、数据上传、WebSocket 连接、分页、重试逻辑、请求中间件、缓存、后台传输或网络可达性监控。也适用于处理 Swift 应用中的 HTTP 请求、API 客户端、网络错误处理或数据获取。
iOS 网络编程
对于普通的 HTTP、REST、上传、下载和流式传输,使用带有 async/await 和结构化并发的 URLSession。对于底层协议和持久化后台传输,使用 Network.framework 以及 delegate/task API。
目录
核心 URLSession async/await
URLSession 在 iOS 15 中增加了原生 async/await 重载。对于前台数据、上传、下载和流式传输,优先使用这些方法。后台 URLSession 传输是主要例外:它们仍然使用 task/delegate API,以便系统在挂起或重启后传递事件。
使用 URLProtocol 固定测试数据在本地验证网络策略,包括有效 2xx、格式错误 2xx、一次性 401 刷新、有限 429/5xx 重试、超时/离线、取消和不可重试 4xx。检查头部、状态和错误分类;修复策略并重新运行。仅重试安全/幂等或明确可重放的请求,且绝不循环令牌刷新。
数据请求
// 基本 GET
let (data, response) = try await URLSession.shared.data(from: url)
// 使用配置的 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)
响应验证
在解码前始终验证 HTTP 状态码。URLSession 不会为 4xx/5xx 响应抛出异常——它仅在传输层失败时抛出。
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
)
}
使用 Codable 进行 JSON 解码
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)
}
下载和上传
对于大文件,使用 download(for:)——它会流式传输到磁盘,而不是将整个负载加载到内存中。
// 下载到临时文件
let (localURL, response) = try await URLSession.shared.download(for: request)
// 立即移动或复制返回的临时文件。
let destination = documentsDirectory.appendingPathComponent("file.zip")
try FileManager.default.moveItem(at: localURL, to: destination)
对于基于 delegate 的 URLSessionDownloadDelegate,在 urlSession(_:downloadTask:didFinishDownloadingTo:) 返回之前移动或打开临时文件。
后台会话是基于 delegate 的传输队列。使用任务创建 API,例如 downloadTask(with:) 和基于文件的 uploadTask(with:fromFile:),然后处理 URLSessionDelegate / 任务 delegate 回调。不要使用异步便捷 API,例如 data(for:)、download(for:) 或 upload(for:),因为这是持久化后台会话模式。
// 上传数据
let (data, response) = try await URLSession.shared.upload(for: request, from: bodyData)
// 从文件上传
let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
使用 AsyncBytes 进行流式传输
对于流式响应、进度跟踪或行分隔数据(例如服务器发送事件),使用 bytes(for:)。
let (bytes, response) = try await URLSession.shared.bytes(for: request)
for try await line in bytes.lines {
// 处理到达的每一行(例如 SSE 流)
handleEvent(line)
}
API 客户端架构
基于协议的客户端
为可测试性定义协议。这允许你在测试中交换实现,而无需直接模拟 URLSession。
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
}
struct Endpoint: Sendable {
let path: String
var method: String = "GET"
var queryItems: [URLQueryItem] = []
var headers: [String: String] = [:]
func url(relativeTo baseURL: URL) -> URL {
guard let components = URLComponents(
url: baseURL.appendingPathComponent(path),
resolvingAgainstBaseURL: true
) else {
preconditionFailure("路径 \(path) 的 URL 组件无效")
}
var mutableComponents = components
if !queryItems.isEmpty {
mutableComponents.queryItems = queryItems
}
guard let url = mutableComponents.url else {
preconditionFailure("无法从组件构造 URL")
}
return url
}
}
客户端接受 baseURL、可选的 URLSession、JSONDecoder 和 RequestMiddleware 拦截器数组。每个方法从端点构建 URLRequest,应用中间件,执行请求,验证状态码并解码结果。完整的 APIClient 实现(包括便捷方法、请求构建器和测试设置)请参见 references/urlsession-patterns.md。
生产客户端应接收注入的、配置好的 URLSession,而不是内部调用 URLSession.shared。配置 URLSessionConfiguration,包括请求/资源超时、缓存策略或 URLCache、waitsForConnectivity、数据成本策略,以及在需要处理身份验证挑战、重定向、指标、证书固定或后台传输时的 delegate。
轻量级闭包客户端
对于使用 MV 模式的应用程序,使用基于闭包的客户端以实现可测试性和 SwiftUI 预览支持。完整模式(异步闭包结构体,通过 init 注入)请参见 references/lightweight-clients.md。
请求中间件 / 拦截器
中间件在请求发送前对其进行转换。用于身份验证、日志记录、分析标头等横切关注点。
protocol RequestMiddleware: Sendable {
func prepare(_ request: URLRequest) async throws -> URLRequest
}
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
}
}
令牌刷新流程
通过刷新令牌并重试一次来处理 401 响应。
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)
}
}
错误处理
结构化错误类型
enum NetworkError: Error, Sendable {
case invalidResponse
case httpError(statusCode: Int, data: Data)
case decodingFailed(Error)
case noConnection
case timedOut
case cancelled
/// 将 URLError 映射为类型化的 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())
}
}
}
关键 URLError 情况
| URLError Code | 含义 | 操作 |
|---|---|---|
.notConnectedToInternet |
设备离线 | 显示离线 UI,排队等待重试 |
.networkConnectionLost |
请求中途连接断开 | 带退避重试 |
.timedOut |
服务器未及时响应 | 重试一次,然后显示错误 |
.cancelled |
任务被取消 | 无需操作;不显示错误 |
.cannotFindHost |
DNS 失败 | 检查 URL,显示错误 |
.secureConnectionFailed |
TLS 握手失败 | 检查证书固定、ATS 配置 |
.userAuthenticationRequired |
需要身份验证才能访问资源 | 触发认证流程 |
解码服务器错误体
struct APIErrorResponse: Decodable, Sendable {
let code: String
let message: String
}
func decodeAPIError(from data: Data) -> APIErrorResponse? {
try? JSONDecoder().decode(APIErrorResponse.self, from: data)
}
// 在 catch 块中使用
catch NetworkError.httpError(let statusCode, let data) {
if let apiError = decodeAPIError(from: data) {
showError("服务器错误:\(apiError.message)")
} else {
showError("HTTP \(statusCode)")
}
}
带指数退避的重试
使用结构化并发进行重试。在尝试之间尊重任务取消。跳过取消和 4xx 客户端错误(429 除外)的重试。
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!
}
分页
使用 AsyncSequence 构建基于游标或偏移量的分页。始终在页面之间检查 Task.isCancelled。完整的 CursorPaginator 和基于偏移量的实现请参见 references/urlsession-patterns.md。
网络可达性
使用 Network 框架的 NWPathMonitor——而不是第三方 Reachability 库。在当前 OS 目标上,它符合 AsyncSequence;仅为了兼容性或自定义投影才包装 pathUpdateHandler。
import Network
func observeNetworkStatus() async {
let monitor = NWPathMonitor()
for await path in monitor {
handle(path.status)
}
}
检查 path.isExpensive(蜂窝网络)和 path.isConstrained(低数据模式)以调整行为(降低图像质量,跳过预取)。
对于底层 TCP、UDP、监听器、Bonjour、路径监控或 WebSocket 协议工作,使用 Network.framework——而不是普通的 REST API。对于 iOS 26 的 NetworkConnection<QUIC>,openStream(...) 和 inboundStreams(...) 是异步抛出 API;请参见 references/network-framework.md#quic-multiplexed-streams。
配置 URLSession
当生产代码需要超时、缓存、连接等待、数据成本策略、身份验证挑战、重定向、指标或后台 delegate 时,注入一个配置好的会话。仅对简单的一次性工作使用 URLSession.shared。完整的配置和测试设置请参见 URLSession patterns。
应用传输安全 (ATS)
ATS 使 HTTPS 成为 URL 加载系统的默认设置。不要启用全局任意加载;使用最窄的合理域/本地网络异常。为 Network.framework 显式配置 TLS。将深度信任和 SPKI 固定设计保留在 swift-security 中。
常见错误
不要: 对动态输入强制解包 URL(string:)。
应该: 使用 URL(string:) 并妥善处理错误。仅对编译时常量字符串可以强制解包。
不要: 在主线程上解码大型 JSON 负载。
应该: 将解码保持在 URLSession 调用的上下文中,默认情况下不在主线程。仅在需要更新 UI 状态时切换到 @MainActor。
不要: 在长时间运行的网络任务中忽略取消。
应该: 在循环(分页、流式传输、重试)中检查 Task.isCancelled 或调用 try Task.checkCancellation()。在 SwiftUI 中使用 .task 实现自动取消。
不要: 在 URLSession async/await 可以满足需求时使用 Alamofire 或 Moya。
应该: 直接使用 URLSession。有了 async/await,证明第三方库合理的人体工程学差距已不复存在。将第三方库保留用于真正缺失的功能(例如图像缓存)。
不要: 在测试中直接模拟 URLSession。
应该: 使用 URLProtocol 子类进行传输层模拟,或使用接受测试替身的基于协议的客户端。
不要: 从 body 或视图初始化器中发起网络请求。
应该: 使用 .task 或 .task(id:) 触发网络调用。
审查清单
- [ ] 前台传输使用 async/await;后台会话使用 delegate/task API
- [ ] 错误处理涵盖 URLError 情况(.notConnectedToInternet、.timedOut、.cancelled)
- [ ] 请求可取消(通过
.task修饰符或存储的 Task 引用尊重 Task 取消) - [ ] 身份验证令牌通过中间件注入,而非硬编码
- [ ] 在解码前验证响应 HTTP 状态码
- [ ] 大文件下载使用
download(for:)而非data(for:) - [ ] 网络调用在
@MainActor之外进行(仅 UI 更新在主线程) - [ ] URLSession 配置了适当的超时和缓存
- [ ] 生产客户端注入配置好的会话,而非使用
URLSession.shared - [ ] 后台传输使用 task/delegate API,而非异步便捷 API
- [ ] 重试逻辑排除取消和 4xx 客户端错误
- [ ] 分页在页面之间检查
Task.isCancelled - [ ] 敏感令牌存储在钥匙串中(而非 UserDefaults 或纯文本文件)
- [ ] 没有对动态输入强制解包 URL
- [ ] 服务器错误响应被解码并呈现给用户
- [ ] Network.framework 代码显式配置 TLS/信任,并将深度固定工作保留在
swift-security中 - [ ]
NetworkConnection<QUIC>流 API 被视为异步抛出 - [ ] 确保网络响应模型类型符合 Sendable;对更新 UI 的完成路径使用 @MainActor
参考资料
- 完整的 API 客户端实现、多部分上传、下载进度、URLProtocol 模拟、重试/退避、证书固定、请求日志和分页实现,请参见 references/urlsession-patterns.md。
- 后台 URLSession 配置、后台下载/上传、带结构化并发的 WebSocket 模式以及重连策略,请参见 references/background-websocket.md。
- 轻量级闭包客户端模式(异步闭包结构体,通过 init 注入以实现可测试性和预览支持),请参见 references/lightweight-clients.md。
- Network.framework(NWConnection、NWListener、NWBrowser、NWPathMonitor)和底层 TCP/UDP/WebSocket 模式,请参见 references/network-framework.md。
- 文件系统目录选择、FileProtectionType、备份排除和存储压力处理,请参见 references/file-storage-patterns.md。






