在 iOS/macOS 應用程式中,使用 URLSession 搭配 async/await、結構化並行與現代 Swift 模式來建置、審查或改進網路程式碼。適用於處理 REST API、下載檔案、上傳資料、WebSocket 連線、分頁、重試邏輯、請求中介層、快取、背景傳輸或網路連線狀態監控。也適用於處理 HTTP 請求、API 客戶端、網路錯誤處理或在 Swift 應用程式中擷取資料。
iOS 網路程式設計
對於一般的 HTTP、REST、上傳、下載與串流,使用 URLSession 搭配 async/await 與結構化並行。對於較低階的協定與可持久的背景傳輸,使用 Network.framework 以及 delegate/task API。
目錄
- 核心 URLSession async/await
- API 客戶端架構
- 錯誤處理
- 分頁
- 網路連線狀態
- 設定 URLSession
- App Transport Security (ATS)
- 常見錯誤
- 審查清單
- 參考資料
核心 URLSession async/await
URLSession 在 iOS 15 中加入了原生的 async/await 多載。在前景的資料、上傳、下載與串流工作中,優先使用這些多載。背景 URLSession 傳輸是主要的例外:它們仍然使用 task/delegate API,以便系統在暫停或重新啟動後仍能傳遞事件。
使用 URLProtocol 測試裝置來驗證網路政策,涵蓋有效的 2xx、格式錯誤的 2xx、一次性 401 重新整理、有限度的 429/5xx 重試、逾時/離線、取消以及不可重試的 4xx。檢查標頭、狀態與錯誤分類;修正政策後重新執行。僅重試安全/冪等或明確可重播的請求,且絕不無窮迴圈重新整理 token。
資料請求
// 基本 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:) 回傳之前移動或開啟暫存檔案。
背景 session 是由 delegate 驅動的傳輸佇列。使用任務建立 API,例如 downloadTask(with:) 與基於檔案的 uploadTask(with:fromFile:),然後處理 URLSessionDelegate / 任務 delegate 回呼。請勿使用非同步便利 API,例如 data(for:)、download(for:) 或 upload(for:),因為它們不適用於可持久的背景 session 模式。
// 上傳資料
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
}
}
Token 重新整理流程
處理 401 回應的方式是重新整理 token 並重試一次。
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 時,注入已設定的 session。僅在簡單的一次性工作使用 URLSession.shared。完整的設定與測試設定,請參閱 URLSession patterns。
App Transport Security (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;背景 session 使用 delegate/task API
- [ ] 錯誤處理涵蓋 URLError 案例(.notConnectedToInternet、.timedOut、.cancelled)
- [ ] 請求可取消(透過
.task修飾詞或儲存的 Task 參考尊重 Task 取消) - [ ] 認證 token 透過中介層注入,而非硬編碼
- [ ] 回應 HTTP 狀態碼在解碼前已驗證
- [ ] 大型下載使用
download(for:)而非data(for:) - [ ] 網路呼叫在
@MainActor之外進行(僅 UI 更新在主執行緒) - [ ] URLSession 已設定適當的逾時與快取
- [ ] 正式環境客戶端注入已設定的 session,而非使用
URLSession.shared - [ ] 背景傳輸使用 task/delegate API,而非非同步便利 API
- [ ] 重試邏輯排除取消與 4xx 用戶端錯誤
- [ ] 分頁在頁面之間檢查
Task.isCancelled - [ ] 敏感 token 儲存在 Keychain 中(而非 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。




