关于 ios-networking
面向 iOS 开发者的网络编程指南,说明如何用 URLSession 和结构化并发处理常见 HTTP 请求、上传、下载与流式响应。也涵盖 API 客户端设计、错误处理、重试和 Network.framework。
主要功能
- 01URLSession 异步请求与传输
- 02API 客户端与中间件设计
- 03状态码及网络错误处理
- 04取消、重试与分页模式
- 05Network.framework 使用指引
适用场景
- 01实现 REST API 客户端
- 02下载大文件或上传数据
- 03处理 SSE 等流式响应
- 04测试网络错误与重试策略
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill ios-networking安装后在 Claude Code 中输入/ios-networking确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/background-websocket.mdfile-storage-patterns.mdlightweight-clients.mdnetwork-framework.mdurlsession-patterns.md详细介绍
iOS 网络
对于常规 HTTP、REST、上传、下载和流式处理,请使用支持 async/await 和结构化并发的 URLSession。对于较底层的协议,请使用 Network.framework;对于需要持久执行的后台传输,请使用代理和任务 API。
目录
URLSession async/await 基础
URLSession 在 iOS 15 中新增了原生的 async/await 重载。前台数据、上传、下载和流式处理优先使用这些重载。后台 URLSession 传输是主要例外:它仍使用任务和代理 API,以便系统能在应用挂起或重新启动后传递事件。
使用 URLProtocol 测试样例,在本地验证网络策略,涵盖有效的 2xx、格式错误的 2xx、一次性 401 刷新、有界的 429/5xx 重试、超时/离线、取消以及不可重试的 4xx。检查标头、状态码和错误分类;修正策略后重新运行。仅对安全/幂等或明确可重放的请求进行重试,且绝不要循环刷新令牌。
数据请求
// 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)
响应验证
解码前务必验证 HTTP 状态码。对于 4xx/5xx 响应,URLSession 不会抛出错误——它只会对传输层故障抛出错误。
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:)——它会将数据流式写入磁盘,而不是把整个数据载荷加载到内存中。
// Download to a temporary file
let (localURL, response) = try await URLSession.shared.download(for: request)
// Move or copy the returned temporary file promptly.
let destination = documentsDirectory.appendingPathComponent("file.zip")
try FileManager.default.moveItem(at: localURL, to: destination)
对于基于代理的 URLSessionDownloadDelegate,请务必在 urlSession(_:downloadTask:didFinishDownloadingTo:) 返回前移动或打开临时文件。
后台会话是由代理驱动的传输队列。使用 downloadTask(with:) 和基于文件的 uploadTask(with:fromFile:) 等任务创建 API,然后处理 URLSessionDelegate / 任务代理回调。请勿将 data(for:)、download(for:) 或 upload(for:) 等异步便捷 API 用作持久后台会话模式。
// 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)
使用 AsyncBytes 进行流式处理
对于流式响应、进度跟踪或按行分隔的数据(例如服务器发送事件),请使用 bytes(for:)。
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 客户端架构
基于协议的客户端
定义协议以便于测试。这样无需直接模拟 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("Invalid URL components for path: \(path)")
}
var mutableComponents = components
if !queryItems.isEmpty {
mutableComponents.queryItems = queryItems
}
guard let url = mutableComponents.url else {
preconditionFailure("Failed to construct URL from components")
}
return url
}
}
客户端接收 baseURL、可选的自定义 URLSession、JSONDecoder,以及 RequestMiddleware 拦截器数组。每个方法都会根据端点构建 URLRequest、应用中间件、执行请求、验证状态码并解码结果。完整的 APIClient 实现(含便捷方法、请求构建器和测试设置)请参阅 references/urlsession-patterns.md。
生产环境中的客户端应接收注入的、已配置好的 URLSession,而不是在内部调用 URLSession.shared。配置 URLSessionConfiguration,设置请求/资源超时时间、缓存策略或 URLCache、waitsForConnectivity、数据成本策略;当身份验证质询、重定向、指标、固定证书或后台传输处理很重要时,还应配置代理。
轻量级闭包式客户端
对于使用 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
/// 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())
}
}
}
主要的 URLError 情况
| URLError 代码 | 含义 | 操作 |
|---|---|---|
.notConnectedToInternet | 设备离线 | 显示离线界面,加入重试队列 |
.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)
}
// 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)")
}
}
使用指数退避重试
使用结构化并发进行重试。在两次尝试之间遵循任务取消状态。跳过因取消和 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
当生产代码需要设置超时、缓存、等待连接、数据成本策略、身份验证质询、重定向、指标或后台代理时,注入已配置的会话。仅在简单的一次性任务中使用 URLSession.shared。完整配置和测试设置,请参阅 URLSession 模式。
App Transport Security(ATS)
ATS 会将 HTTPS 设为 URL Loading System 的默认方式。不要启用不加限制的任意加载;仅针对有充分理由的域名或本地网络配置范围最窄的例外。为 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;后台会话使用委托/任务 API
- 错误处理涵盖 URLError 情况(.notConnectedToInternet、.timedOut、.cancelled)
- 请求可取消(通过
.task修饰符或存储的 Task 引用响应 Task 取消) - 通过中间件注入身份验证令牌,而非硬编码
- 解码前验证响应的 HTTP 状态码
- 大型下载使用
download(for:),而非data(for:) - 网络调用在
@MainActor之外执行(仅在主线程上更新 UI) - URLSession 配置了适当的超时和缓存
- 生产客户端注入已配置的会话,而非使用
URLSession.shared - 后台传输使用任务/委托 API,而非异步便捷 API
- 重试逻辑不包括取消和 4xx 客户端错误
- 分页会在每页之间检查
Task.isCancelled - 敏感令牌存储在 Keychain 中(而非 UserDefaults 或纯文本文件)
- 不会对动态输入的 URL 使用强制解包
- 解码服务器错误响应并呈现给用户
- Network.framework 代码明确配置 TLS/信任,并将深度证书固定相关工作保留在
swift-security中 - 将
NetworkConnection<QUIC>流 API 视为异步抛出错误的 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。