ios-networking について
iOS 開発者向けのネットワークガイドです。URLSession と構造化並行処理による HTTP リクエスト、アップロード、ダウンロード、ストリーミングを説明し、API クライアント設計、エラー処理、再試行、Network.framework も扱います。
主な機能
- 01URLSession の非同期リクエストと転送
- 02API クライアントとミドルウェア設計
- 03HTTP ステータスとネットワークエラー処理
- 04キャンセル・再試行・ページネーションのパターン
- 05Network.framework の指針
活用シーン
- 01REST API クライアントの実装
- 02大容量ファイルのダウンロードやデータのアップロード
- 03SSE などのストリーミング応答の処理
- 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
- API クライアントのアーキテクチャ
- エラー処理
- ページネーション
- ネットワーク到達可能性
- URLSession の設定
- App Transport Security(ATS)
- よくある間違い
- 確認チェックリスト
- 参考資料
URLSession の基本的な async/await
URLSession には iOS 15 で、ネイティブの async/await オーバーロードが追加されました。フォアグラウンドでのデータ取得、アップロード、ダウンロード、ストリーミングには、これらを優先して使用してください。主な例外はバックグラウンドの URLSession 転送です。システムが一時停止後や再起動後にもイベントを配信できるよう、引き続きタスク/デリゲート API を使用します。
有効な 2xx、不正な 2xx、1 回限りの 401 リフレッシュ、上限付きの 429/5xx リトライ、タイムアウト/オフライン、キャンセル、リトライ不可の 4xx を対象に、URLProtocol のフィクスチャを使ってネットワークポリシーをローカルで検証します。ヘッダー、ステータス、エラー分類を確認し、ポリシーを修正して再実行します。リトライは安全/冪等なリクエスト、または明示的に再送可能なリクエストに限り、トークンのリフレッシュをループさせないでください。
データリクエスト
// 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 ステータスコードを検証してください。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:) を使用してください。ペイロード全体をメモリに読み込まず、ディスクにストリーミングします。
// 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.shared を呼び出すのではなく、設定済みの URLSession を注入してください。認証チャレンジ、リダイレクト、メトリクス、ピンニング、バックグラウンド転送の処理が必要な場合は、リクエスト/リソースのタイムアウト、キャッシュポリシーまたは URLCache、waitsForConnectivity、データコストポリシー、デリゲートを URLSessionConfiguration で設定します。
軽量なクロージャーベースのクライアント
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 | デバイスがオフライン | オフライン 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)
}
// 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 patterns を参照してください。
App Transport Security(ATS)
ATS により、URL Loading System では HTTPS がデフォルトになります。任意のロードを一律に有効にせず、正当化できる最も限定的なドメインまたはローカルネットワークの例外を使用してください。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 レスポンスのステータスコードを検証する
- 大きなダウンロードには
data(for:)ではなくdownload(for:)を使用する - ネットワーク呼び出しは
@MainActorの外で実行する(メインスレッドでは UI の更新のみを行う) - 適切なタイムアウトとキャッシュを設定して URLSession を構成する
- 本番環境のクライアントには、
URLSession.sharedを使わず、構成済みのセッションを注入する - バックグラウンド転送には非同期の簡易 API ではなく、タスク/デリゲート API を使用する
- リトライロジックではキャンセルと 4xx のクライアントエラーを除外する
- ページネーションではページ間で
Task.isCancelledを確認する - 機密性の高いトークンは 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 を参照してください。