ios-networking by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill ios-networking适用于 iOS 26+ 的现代网络编程模式,使用 URLSession 配合 async/await 和结构化并发。所有示例均基于 Swift 6.2。无需第三方依赖——URLSession 覆盖了绝大部分网络需求。
URLSession 在 iOS 15 中获得了原生的 async/await 重载。这些是新代码中唯一应该使用的网络 API。在新项目中切勿使用完成处理程序(completion-handler)变体。
// 基本 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
)
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
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)
// 上传数据
let (data, response) = try await URLSession.shared.upload(for: request, from: bodyData)
// 从文件上传
let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
对于流式响应、进度跟踪或行分隔数据(例如,服务器发送事件),使用 bytes(for:)。
let (bytes, response) = try await URLSession.shared.bytes(for: request)
for try await line in bytes.lines {
// 处理到达的每一行(例如,SSE 流)
handleEvent(line)
}
为可测试性定义一个协议。这允许你在测试中交换实现,而无需直接模拟 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。
对于使用 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 代码 | 含义 | 操作 |
|---|---|---|
.notConnectedToInternet | 设备离线 | 显示离线 UI,加入重试队列 |
.networkConnectionLost | 连接在请求过程中断开 | 使用退避策略重试 |
.timedOut | 服务器未在规定时间内响应 | 重试一次,然后显示错误 |
.cancelled | 任务被取消 | 无需操作;不显示错误 |
.cannotFindHost | DNS 解析失败 | 检查 URL,显示错误 |
.secureConnectionFailed | TLS 握手失败 | 检查证书固定、ATS 配置 |
.userAuthenticationRequired | 代理返回 401 | 触发身份验证流程 |
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——而非第三方可达性库。将其包装在 AsyncStream 中以适配结构化并发。
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(低数据模式)以调整行为(降低图像质量,跳过预取)。
为生产代码创建配置好的会话。URLSession.shared 仅适用于简单的一次性请求。
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 反馈。
不要: 在有自定义配置需求时使用 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,曾经证明使用第三方库合理性的易用性差距已不复存在。将第三方库保留用于真正缺失的功能(例如,图像缓存)。
不要: 在测试中直接模拟 URLSession。要: 使用 URLProtocol 子类进行传输层模拟,或者使用接受测试替身的基于协议的客户端。
不要: 对大文件下载使用 data(for:)。要: 使用 download(for:),它会流式写入磁盘并避免内存峰值。
不要: 从 body 或视图初始化器中触发网络请求。要: 使用 .task 或 .task(id:) 来触发网络调用。
不要: 在请求中硬编码身份验证令牌。要: 通过中间件注入令牌,以便它们集中管理且可刷新。
不要: 忽略 HTTP 状态码并盲目解码。要: 在解码前验证状态码。带有无效 JSON 的 200 响应和带有错误体的 500 响应需要不同的处理方式。
.task 修饰符或存储的 Task 引用尊重 Task 取消)download(for:) 而非 data(for:)@MainActor 之外(仅 UI 更新在主线程)Task.isCancelledreferences/urlsession-patterns.md。references/background-websocket.md。references/lightweight-clients.md。references/network-framework.md。每周安装量
399
代码仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex392
opencode391
kimi-cli389
amp389
cline389
github-copilot389
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.
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.
// 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)
Always validate the HTTP status code before decoding. URLSession does not throw for 4xx/5xx responses -- it only throws for transport-level failures.
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
)
}
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)
}
Use download(for:) for large files -- it streams to disk instead of loading the entire payload into memory.
// 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)
// 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)
Use bytes(for:) for streaming responses, progress tracking, or line-delimited data (e.g., server-sent events).
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)
}
Define a protocol for testability. This lets you swap implementations in tests without mocking URLSession directly.
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
}
}
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.
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).
Middleware transforms requests before they are sent. Use this for authentication, logging, analytics headers, and similar cross-cutting concerns.
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
}
}
Handle 401 responses by refreshing the token and retrying once.
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 Code | Meaning | Action |
|---|---|---|
.notConnectedToInternet | Device offline | Show offline UI, queue for retry |
.networkConnectionLost | Connection 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 |
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)")
}
}
Use structured concurrency for retries. Respect task cancellation between attempts. Skip retries for cancellation and 4xx client errors (except 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!
}
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.
Use NWPathMonitor from the Network framework — not third-party Reachability libraries. Wrap in AsyncStream for structured concurrency.
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).
Create a configured session for production code. URLSession.shared is acceptable only for simple, one-off requests.
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.
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.
.task modifier or stored Task references)download(for:) not data(for:)@MainActor (only UI updates on main)Task.isCancelled between pagesreferences/urlsession-patterns.md for complete API client implementation, multipart uploads, download progress, URLProtocol mocking, retry/backoff, certificate pinning, request logging, and pagination implementations.references/background-websocket.md for background URLSession configuration, background downloads/uploads, WebSocket patterns with structured concurrency, and reconnection strategies.references/lightweight-clients.md for the lightweight closure-based client pattern (struct of async closures, injected via init for testability and preview support).references/network-framework.md for Network.framework (NWConnection, NWListener, NWBrowser, NWPathMonitor) and low-level TCP/UDP/WebSocket patterns.Weekly Installs
399
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex392
opencode391
kimi-cli389
amp389
cline389
github-copilot389
飞书OpenAPI Explorer:探索和调用未封装的飞书原生API接口
15,500 周安装
| DNS failure |
| Check URL, show error |
.secureConnectionFailed | TLS handshake failed | Check cert pinning, ATS config |
.userAuthenticationRequired | 401 from proxy | Trigger auth flow |