mcpField
I
ios-networking

ios-networking

github.com/dpearson2699开发1.2k4.4k 次安装13 周前更新
代码仓库

关于 ios-networking

面向 iOS 开发者的网络编程指南,说明如何用 URLSession 和结构化并发处理常见 HTTP 请求、上传、下载与流式响应。也涵盖 API 客户端设计、错误处理、重试和 Network.framework。

主要功能

  1. 01URLSession 异步请求与传输
  2. 02API 客户端与中间件设计
  3. 03状态码及网络错误处理
  4. 04取消、重试与分页模式
  5. 05Network.framework 使用指引

适用场景

  1. 01实现 REST API 客户端
  2. 02下载大文件或上传数据
  3. 03处理 SSE 等流式响应
  4. 04测试网络错误与重试策略

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill ios-networking

安装后在 Claude Code 中输入/ios-networking确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
background-websocket.md
file-storage-patterns.md
lightweight-clients.md
network-framework.md
urlsession-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任务已取消无需操作;不要显示错误
.cannotFindHostDNS 失败检查 URL,显示错误
.secureConnectionFailedTLS 握手失败检查证书固定和 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。

评论

支持 Markdown