URLSession
Appleが提供するiOS/macOS標準のHTTPクライアントAPI。Foundationフレームワークの一部として、ネイティブで高性能なHTTP通信を実現。async/await対応、HTTP/2サポート、バックグラウンド転送、詳細な設定オプション、Cookie管理機能を内蔵。
GitHub概要
スター69,178
ウォッチ2,433
フォーク10,560
作成日:2015年10月23日
言語:C++
ライセンス:Apache License 2.0
トピックス
なし
スター履歴
データ取得日時: 2025/10/22 04:10
ライブラリ
URLSession
概要
URLSessionはSwift Foundationフレームワークの一部として提供される「ネットワークセッションの設定と管理のためのクラス群」です。AppleプラットフォームにおけるHTTPクライアントの事実上の標準として、iOS、macOS、watchOS、tvOSアプリでのネットワーク通信を支援。URL によって識別されるエンドポイントとの間でデータの非同期ダウンロード・アップロードを可能にし、WebSocket通信、バックグラウンドダウンロード、証明書ピニングなど企業レベルのネットワーク機能を包括的にサポートしています。
詳細
URLSession 2025年版は Swift Concurrency (async/await) との完全統合により、従来のコールバックベースAPIに加えて現代的な非同期プログラミングパターンを提供しています。URLSessionDataTask、URLSessionUploadTask、URLSessionDownloadTaskの3つの主要なタスクタイプにより、基本的なHTTPリクエストから大容量ファイル転送まで幅広いネットワーク通信ニーズに対応。バックグラウンド実行、App Transport Security (ATS)、HTTP/2サポート、カスタムプロトコル対応など、モバイルアプリケーション特有の要件を満たす高度な機能を標準で提供します。
主な特徴
- 包括的なタスクタイプ: Data、Upload、Download、WebSocketの多様な通信パターン
- Swift Concurrency統合: async/await による現代的な非同期プログラミング
- バックグラウンド処理: アプリ非アクティブ時の継続的なデータ転送
- セキュリティ強化: ATS、証明書ピニング、TLS 1.3対応
- HTTP/2・HTTP/3サポート: 最新プロトコルによる高速通信
- Combineフレームワーク統合: Reactive programmingパターン対応
メリット・デメリット
メリット
- Apple プラットフォーム標準による完全統合と最適化
- Swift Concurrency との自然な統合による優れた開発体験
- バックグラウンドダウンロードとモバイル特化機能の充実
- App Store審査で問題となるセキュリティ要件を標準でクリア
- WebSocketやHTTP/3など最新プロトコルの先進的サポート
- メモリ管理とパフォーマンスの最適化が Apple によって保証
デメリット
- Apple プラットフォームに限定され他OSでは利用不可
- 過度に詳細な設定が必要で軽量な用途には複雑
- デバッグ時のネットワークログが限定的
- サードパーティライブラリと比較してカスタマイズ性に制約
- レガシーなAPIとの共存による学習コストの増大
- SwiftUIとの直接的な統合機能が限定的
参考ページ
書き方の例
基本設定とインポート
import Foundation
import Combine // Publisher使用時
// iOS 15+ / macOS 12+ でのasync/await対応
@available(iOS 15.0, macOS 12.0, *)
class NetworkManager {
static let shared = NetworkManager()
private init() {}
// 基本的なURLSessionインスタンス
private let session = URLSession.shared
// カスタム設定のURLSession
private lazy var customSession: URLSession = {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
config.waitsForConnectivity = true
return URLSession(configuration: config)
}()
}
// レスポンス用のSwift構造体
struct User: Codable {
let id: Int
let name: String
let email: String
let age: Int?
}
struct APIResponse<T: Codable>: Codable {
let success: Bool
let data: T
let message: String
}
struct ErrorResponse: Codable {
let error: String
let code: Int
let details: String?
}
基本的なリクエスト(GET/POST/PUT/DELETE)
import Foundation
extension NetworkManager {
// MARK: - Async/Await API (iOS 15+)
// 基本的なGETリクエスト
func fetchUsers() async throws -> [User] {
guard let url = URL(string: "https://api.example.com/users") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("Bearer your-token", forHTTPHeaderField: "Authorization")
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard httpResponse.statusCode == 200 else {
throw URLError(.init(rawValue: httpResponse.statusCode))
}
let users = try JSONDecoder().decode([User].self, from: data)
return users
}
// クエリパラメータ付きGETリクエスト
func fetchUsersWithParams(page: Int, limit: Int) async throws -> [User] {
var components = URLComponents(string: "https://api.example.com/users")!
components.queryItems = [
URLQueryItem(name: "page", value: String(page)),
URLQueryItem(name: "limit", value: String(limit)),
URLQueryItem(name: "sort", value: "created_at")
]
guard let url = components.url else {
throw URLError(.badURL)
}
let (data, response) = try await session.data(from: url)
if let httpResponse = response as? HTTPURLResponse {
print("レスポンスステータス: \(httpResponse.statusCode)")
print("レスポンスヘッダー: \(httpResponse.allHeaderFields)")
}
return try JSONDecoder().decode([User].self, from: data)
}
// POSTリクエスト(JSON送信)
func createUser(_ user: User) async throws -> User {
guard let url = URL(string: "https://api.example.com/users") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("Bearer your-token", forHTTPHeaderField: "Authorization")
// リクエストボディのJSONエンコード
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
request.httpBody = try encoder.encode(user)
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
if httpResponse.statusCode == 201 {
let createdUser = try JSONDecoder().decode(User.self, from: data)
print("ユーザー作成完了: \(createdUser.name)")
return createdUser
} else {
// エラーレスポンスの処理
let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: data)
throw NSError(domain: "APIError", code: errorResponse.code, userInfo: [
NSLocalizedDescriptionKey: errorResponse.error
])
}
}
// PUTリクエスト(データ更新)
func updateUser(id: Int, updates: [String: Any]) async throws -> User {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer your-token", forHTTPHeaderField: "Authorization")
// Dictionaryを直接JSONに変換
request.httpBody = try JSONSerialization.data(withJSONObject: updates)
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(User.self, from: data)
}
// DELETEリクエスト
func deleteUser(id: Int) async throws {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
request.setValue("Bearer your-token", forHTTPHeaderField: "Authorization")
let (_, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 204 else {
throw URLError(.badServerResponse)
}
print("ユーザー削除完了")
}
}
// MARK: - 従来のコールバックベースAPI(iOS 13+)
extension NetworkManager {
// レガシーサポート用のコールバックベースリクエスト
func fetchUsersLegacy(completion: @escaping (Result<[User], Error>) -> Void) {
guard let url = URL(string: "https://api.example.com/users") else {
completion(.failure(URLError(.badURL)))
return
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
completion(.failure(URLError(.badServerResponse)))
return
}
do {
let users = try JSONDecoder().decode([User].self, from: data)
completion(.success(users))
} catch {
completion(.failure(error))
}
}
task.resume()
}
}
高度な設定とカスタマイズ(認証、タイムアウト、キャッシュ等)
import Foundation
import Network
class AdvancedNetworkManager {
// MARK: - カスタムURLSessionConfiguration
static func createCustomSession() -> URLSession {
let config = URLSessionConfiguration.default
// タイムアウト設定
config.timeoutIntervalForRequest = 30.0
config.timeoutIntervalForResource = 120.0
// 接続性設定
config.waitsForConnectivity = true
config.allowsCellularAccess = true
config.allowsExpensiveNetworkAccess = true
config.allowsConstrainedNetworkAccess = false
// キャッシュ設定
let cache = URLCache(
memoryCapacity: 50 * 1024 * 1024, // 50MB
diskCapacity: 200 * 1024 * 1024, // 200MB
diskPath: "networking_cache"
)
config.urlCache = cache
config.requestCachePolicy = .reloadIgnoringLocalCacheData
// ヘッダー設定
config.httpAdditionalHeaders = [
"User-Agent": "MyApp/1.0 (iOS)",
"Accept-Language": "ja-JP,en-US",
"X-API-Version": "v2"
]
// HTTP設定
config.httpMaximumConnectionsPerHost = 6
config.httpShouldUsePipelining = true
config.httpShouldSetCookies = true
config.httpCookieAcceptPolicy = .always
return URLSession(configuration: config)
}
// MARK: - バックグラウンドダウンロード設定
static func createBackgroundSession() -> URLSession {
let config = URLSessionConfiguration.background(
withIdentifier: "com.myapp.background-download"
)
// バックグラウンド専用設定
config.isDiscretionary = true // システム最適化を有効
config.sessionSendsLaunchEvents = true
// 接続設定
config.waitsForConnectivity = true
config.allowsCellularAccess = false // WiFiのみ
return URLSession(configuration: config, delegate: nil, delegateQueue: nil)
}
// MARK: - 認証機能
class AuthenticationManager: NSObject, URLSessionDelegate {
// HTTP基本認証
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
let authenticationMethod = challenge.protectionSpace.authenticationMethod
switch authenticationMethod {
case NSURLAuthenticationMethodHTTPBasic:
let credential = URLCredential(
user: "username",
password: "password",
persistence: .forSession
)
completionHandler(.useCredential, credential)
case NSURLAuthenticationMethodServerTrust:
// SSL証明書ピニング
handleServerTrust(challenge: challenge, completionHandler: completionHandler)
default:
completionHandler(.performDefaultHandling, nil)
}
}
private func handleServerTrust(
challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
// 証明書ピニングのカスタム検証
let policy = SecPolicyCreateSSL(true, "api.example.com" as CFString)
SecTrustSetPolicies(serverTrust, policy)
var secResult = SecTrustResultType.invalid
let status = SecTrustEvaluate(serverTrust, &secResult)
if status == errSecSuccess &&
(secResult == .unspecified || secResult == .proceed) {
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
// MARK: - カスタムHTTPヘッダー管理
func createAuthenticatedRequest(url: URL, token: String) -> URLRequest {
var request = URLRequest(url: url)
// 認証ヘッダー
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
// カスタムヘッダー
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-ID")
request.setValue("MyApp/1.0", forHTTPHeaderField: "User-Agent")
// キャッシュ制御
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
return request
}
// MARK: - プロキシ設定(iOS 14+)
@available(iOS 14.0, *)
func configureProxySettings() -> URLSessionConfiguration {
let config = URLSessionConfiguration.default
// HTTPプロキシ設定
let proxySettings: [String: Any] = [
kCFNetworkProxiesHTTPEnable as String: true,
kCFNetworkProxiesHTTPProxy as String: "proxy.example.com",
kCFNetworkProxiesHTTPPort as String: 8080,
kCFNetworkProxiesHTTPSEnable as String: true,
kCFNetworkProxiesHTTPSProxy as String: "proxy.example.com",
kCFNetworkProxiesHTTPSPort as String: 8080
]
config.connectionProxyDictionary = proxySettings
return config
}
}
エラーハンドリングとリトライ機能
import Foundation
import os.log
// カスタムエラー定義
enum NetworkError: LocalizedError {
case invalidURL
case noData
case decodingError(Error)
case serverError(Int, String?)
case networkUnavailable
case timeout
case unauthorized
case rateLimited(retryAfter: TimeInterval?)
var errorDescription: String? {
switch self {
case .invalidURL:
return "無効なURLです"
case .noData:
return "データが取得できませんでした"
case .decodingError(let error):
return "データの解析に失敗しました: \(error.localizedDescription)"
case .serverError(let code, let message):
return "サーバーエラー (\(code)): \(message ?? "不明なエラー")"
case .networkUnavailable:
return "ネットワークに接続できません"
case .timeout:
return "リクエストがタイムアウトしました"
case .unauthorized:
return "認証が必要です"
case .rateLimited(let retryAfter):
let after = retryAfter.map { String(format: "%.0f秒後", $0) } ?? "しばらく"
return "アクセス制限中です。\(after)に再試行してください"
}
}
}
class RobustNetworkManager {
private let session: URLSession
private let logger = Logger(subsystem: "com.myapp.networking", category: "api")
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.waitsForConnectivity = true
self.session = URLSession(configuration: config)
}
// MARK: - リトライ機能付きリクエスト
func fetchWithRetry<T: Codable>(
url: URL,
type: T.Type,
maxRetries: Int = 3,
backoffMultiplier: Double = 2.0
) async throws -> T {
var lastError: Error?
for attempt in 0...maxRetries {
do {
logger.info("API request attempt \(attempt + 1)/\(maxRetries + 1): \(url)")
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.serverError(0, "無効なレスポンス")
}
// ステータスコード別の処理
switch httpResponse.statusCode {
case 200...299:
logger.info("API request successful: \(httpResponse.statusCode)")
return try JSONDecoder().decode(type, from: data)
case 401:
throw NetworkError.unauthorized
case 429:
let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After")
.flatMap { Double($0) }
throw NetworkError.rateLimited(retryAfter: retryAfter)
case 500...599:
if attempt < maxRetries {
let delay = pow(backoffMultiplier, Double(attempt))
logger.warning("Server error \(httpResponse.statusCode), retrying in \(delay)s")
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
continue
}
throw NetworkError.serverError(httpResponse.statusCode, nil)
default:
throw NetworkError.serverError(httpResponse.statusCode, nil)
}
} catch let error as NetworkError {
lastError = error
// リトライ不可能なエラー
if case .unauthorized = error, case .rateLimited = error {
throw error
}
if attempt < maxRetries {
let delay = pow(backoffMultiplier, Double(attempt))
logger.warning("Request failed: \(error), retrying in \(delay)s")
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
} catch {
lastError = error
// ネットワークエラーの場合はリトライ
if (error as NSError).domain == NSURLErrorDomain {
if attempt < maxRetries {
let delay = pow(backoffMultiplier, Double(attempt))
logger.warning("Network error: \(error), retrying in \(delay)s")
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
continue
}
}
throw error
}
}
throw lastError ?? NetworkError.networkUnavailable
}
// MARK: - 包括的エラーハンドリング
func handleResponse<T: Codable>(
data: Data,
response: URLResponse,
type: T.Type
) throws -> T {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.serverError(0, "HTTPレスポンスではありません")
}
logger.info("Response: \(httpResponse.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))")
switch httpResponse.statusCode {
case 200...299:
do {
return try JSONDecoder().decode(type, from: data)
} catch {
logger.error("JSON decoding failed: \(error)")
throw NetworkError.decodingError(error)
}
case 400:
if let errorData = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
throw NetworkError.serverError(400, errorData.error)
}
throw NetworkError.serverError(400, "Bad Request")
case 401:
throw NetworkError.unauthorized
case 404:
throw NetworkError.serverError(404, "リソースが見つかりません")
case 429:
let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After")
.flatMap { Double($0) }
throw NetworkError.rateLimited(retryAfter: retryAfter)
case 500...599:
throw NetworkError.serverError(httpResponse.statusCode, "サーバー内部エラー")
default:
throw NetworkError.serverError(httpResponse.statusCode, "予期しないエラー")
}
}
// MARK: - ネットワーク接続監視
@available(iOS 12.0, *)
func checkNetworkConnectivity() async -> Bool {
let monitor = NWPathMonitor()
let queue = DispatchQueue(label: "network_monitor")
return await withCheckedContinuation { continuation in
monitor.pathUpdateHandler = { path in
let isConnected = path.status == .satisfied
monitor.cancel()
continuation.resume(returning: isConnected)
}
monitor.start(queue: queue)
}
}
}
// 使用例
@MainActor
class ViewModel: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?
private let networkManager = RobustNetworkManager()
func loadUsers() async {
isLoading = true
errorMessage = nil
do {
guard let url = URL(string: "https://api.example.com/users") else {
throw NetworkError.invalidURL
}
users = try await networkManager.fetchWithRetry(
url: url,
type: [User].self,
maxRetries: 3
)
} catch let error as NetworkError {
errorMessage = error.localizedDescription
} catch {
errorMessage = "予期しないエラー: \(error.localizedDescription)"
}
isLoading = false
}
}
ファイル操作とアップロード・ダウンロード
import Foundation
class FileTransferManager {
private let session: URLSession
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForResource = 600 // 10分
self.session = URLSession(configuration: config)
}
// MARK: - ファイルダウンロード
func downloadFile(from url: URL, to destinationURL: URL) async throws {
let (tempURL, response) = try await session.download(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
// 一時ファイルを目的の場所に移動
try FileManager.default.moveItem(at: tempURL, to: destinationURL)
print("ファイルダウンロード完了: \(destinationURL.path)")
}
// 進捗付きダウンロード(URLSessionDownloadDelegate使用)
func downloadWithProgress(from url: URL) -> AsyncThrowingStream<DownloadProgress, Error> {
return AsyncThrowingStream { continuation in
let task = session.downloadTask(with: url) { tempURL, response, error in
if let error = error {
continuation.finish(throwing: error)
return
}
guard let tempURL = tempURL,
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
continuation.finish(throwing: URLError(.badServerResponse))
return
}
// 最終進捗
continuation.yield(DownloadProgress(
bytesWritten: httpResponse.expectedContentLength,
totalBytes: httpResponse.expectedContentLength,
progress: 1.0,
tempURL: tempURL
))
continuation.finish()
}
task.resume()
}
}
// MARK: - ファイルアップロード
func uploadFile(fileURL: URL, to uploadURL: URL) async throws -> UploadResponse {
var request = URLRequest(url: uploadURL)
request.httpMethod = "POST"
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer your-token", forHTTPHeaderField: "Authorization")
let (data, response) = try await session.upload(for: request, fromFile: fileURL)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 || httpResponse.statusCode == 201 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(UploadResponse.self, from: data)
}
// マルチパートフォームデータアップロード
func uploadMultipartForm(
fileURL: URL,
fields: [String: String],
to uploadURL: URL
) async throws -> UploadResponse {
let boundary = UUID().uuidString
var request = URLRequest(url: uploadURL)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer your-token", forHTTPHeaderField: "Authorization")
let multipartData = createMultipartData(
fileURL: fileURL,
fields: fields,
boundary: boundary
)
let (data, response) = try await session.upload(for: request, from: multipartData)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 || httpResponse.statusCode == 201 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(UploadResponse.self, from: data)
}
private func createMultipartData(
fileURL: URL,
fields: [String: String],
boundary: String
) -> Data {
var data = Data()
// フィールドデータの追加
for (key, value) in fields {
data.append("--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
data.append("\(value)\r\n".data(using: .utf8)!)
}
// ファイルデータの追加
let filename = fileURL.lastPathComponent
let mimeType = mimeTypeForFile(url: fileURL)
data.append("--\(boundary)\r\n".data(using: .utf8)!)
data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
if let fileData = try? Data(contentsOf: fileURL) {
data.append(fileData)
}
data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
return data
}
private func mimeTypeForFile(url: URL) -> String {
let pathExtension = url.pathExtension.lowercased()
switch pathExtension {
case "jpg", "jpeg":
return "image/jpeg"
case "png":
return "image/png"
case "pdf":
return "application/pdf"
case "txt":
return "text/plain"
case "json":
return "application/json"
default:
return "application/octet-stream"
}
}
// MARK: - バックグラウンドダウンロード
func backgroundDownload(from url: URL) -> URLSessionDownloadTask {
let backgroundConfig = URLSessionConfiguration.background(
withIdentifier: "com.myapp.background-download"
)
let backgroundSession = URLSession(configuration: backgroundConfig)
let task = backgroundSession.downloadTask(with: url)
task.resume()
return task
}
}
// 関連する型定義
struct DownloadProgress {
let bytesWritten: Int64
let totalBytes: Int64
let progress: Double
let tempURL: URL?
}
struct UploadResponse: Codable {
let fileId: String
let filename: String
let size: Int64
let url: String
}
// SwiftUIでの使用例
struct FileDownloadView: View {
@State private var downloadProgress: Double = 0
@State private var isDownloading = false
private let fileManager = FileTransferManager()
var body: some View {
VStack {
if isDownloading {
ProgressView(value: downloadProgress, total: 1.0)
.progressViewStyle(LinearProgressViewStyle())
Text("ダウンロード中: \(Int(downloadProgress * 100))%")
} else {
Button("ファイルをダウンロード") {
Task {
await startDownload()
}
}
}
}
}
private func startDownload() async {
isDownloading = true
guard let url = URL(string: "https://api.example.com/files/large-file.zip") else {
return
}
do {
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destinationURL = documentsPath.appendingPathComponent("downloaded-file.zip")
try await fileManager.downloadFile(from: url, to: destinationURL)
} catch {
print("ダウンロードエラー: \(error)")
}
isDownloading = false
}
}
Combineフレームワーク統合と実用的な活用例
import Foundation
import Combine
import SwiftUI
// MARK: - Combine Publisher拡張
extension URLSession {
// 汎用APIリクエストPublisher
func apiRequest<T: Codable>(
for request: URLRequest,
responseType: T.Type
) -> AnyPublisher<T, NetworkError> {
return dataTaskPublisher(for: request)
.tryMap { data, response -> T in
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.serverError(0, "無効なレスポンス")
}
guard 200...299 ~= httpResponse.statusCode else {
throw NetworkError.serverError(httpResponse.statusCode, nil)
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw NetworkError.decodingError(error)
}
}
.mapError { error in
if let networkError = error as? NetworkError {
return networkError
} else {
return NetworkError.networkUnavailable
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
// MARK: - CombineベースAPIクライアント
class CombineAPIClient: ObservableObject {
private let session = URLSession.shared
private var cancellables = Set<AnyCancellable>()
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?
// ユーザー一覧取得
func fetchUsers() {
guard let url = URL(string: "https://api.example.com/users") else {
errorMessage = "無効なURL"
return
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
isLoading = true
errorMessage = nil
session.apiRequest(for: request, responseType: [User].self)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] users in
self?.users = users
}
)
.store(in: &cancellables)
}
// リアクティブなユーザー検索
func searchUsers(query: String) -> AnyPublisher<[User], NetworkError> {
guard !query.isEmpty,
let url = URL(string: "https://api.example.com/users/search") else {
return Just([])
.setFailureType(to: NetworkError.self)
.eraseToAnyPublisher()
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let searchData = ["query": query]
request.httpBody = try? JSONSerialization.data(withJSONObject: searchData)
return session.apiRequest(for: request, responseType: [User].self)
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates { $0.count == $1.count }
.eraseToAnyPublisher()
}
// バッチリクエスト処理
func fetchUserDetails(ids: [Int]) {
let publishers = ids.map { id in
fetchUserDetail(id: id)
.catch { _ in Just(nil) }
}
Publishers.MergeMany(publishers)
.collect()
.sink { userDetails in
let validUsers = userDetails.compactMap { $0 }
print("取得したユーザー詳細: \(validUsers.count)件")
}
.store(in: &cancellables)
}
private func fetchUserDetail(id: Int) -> AnyPublisher<User?, Never> {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
return Just(nil).eraseToAnyPublisher()
}
let request = URLRequest(url: url)
return session.apiRequest(for: request, responseType: User.self)
.map { Optional($0) }
.replaceError(with: nil)
.eraseToAnyPublisher()
}
}
// MARK: - SwiftUIとの統合
struct UserListView: View {
@StateObject private var apiClient = CombineAPIClient()
@State private var searchText = ""
@State private var searchResults: [User] = []
var body: some View {
NavigationView {
VStack {
// 検索バー
SearchBar(text: $searchText)
.onChange(of: searchText) { query in
searchUsers(query: query)
}
// ユーザーリスト
if apiClient.isLoading {
ProgressView("読み込み中...")
} else if !searchResults.isEmpty {
List(searchResults, id: \.id) { user in
UserRowView(user: user)
}
} else {
List(apiClient.users, id: \.id) { user in
UserRowView(user: user)
}
}
Spacer()
}
.navigationTitle("ユーザー一覧")
.onAppear {
apiClient.fetchUsers()
}
.alert("エラー", isPresented: .constant(apiClient.errorMessage != nil)) {
Button("OK") {
apiClient.errorMessage = nil
}
} message: {
Text(apiClient.errorMessage ?? "")
}
}
}
private func searchUsers(query: String) {
apiClient.searchUsers(query: query)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in },
receiveValue: { users in
searchResults = users
}
)
.store(in: &apiClient.cancellables)
}
}
struct UserRowView: View {
let user: User
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(user.name)
.font(.headline)
Text(user.email)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
if let age = user.age {
Text("\(age)歳")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 4)
}
}
struct SearchBar: View {
@Binding var text: String
var body: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.secondary)
TextField("ユーザーを検索...", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(.horizontal)
}
}
// MARK: - WebSocket通信例
@available(iOS 13.0, *)
class WebSocketManager: NSObject, ObservableObject {
@Published var connectionStatus: WebSocketConnectionStatus = .disconnected
@Published var receivedMessages: [String] = []
private var webSocketTask: URLSessionWebSocketTask?
private let session = URLSession(configuration: .default)
func connect(to url: URL) {
webSocketTask = session.webSocketTask(with: url)
webSocketTask?.resume()
connectionStatus = .connecting
// メッセージ受信の開始
receiveMessage()
// 接続状態の監視
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.connectionStatus = .connected
}
}
func sendMessage(_ text: String) {
let message = URLSessionWebSocketTask.Message.string(text)
webSocketTask?.send(message) { error in
if let error = error {
print("WebSocket送信エラー: \(error)")
}
}
}
func disconnect() {
webSocketTask?.cancel(with: .normalClosure, reason: nil)
connectionStatus = .disconnected
}
private func receiveMessage() {
webSocketTask?.receive { [weak self] result in
switch result {
case .success(let message):
switch message {
case .string(let text):
DispatchQueue.main.async {
self?.receivedMessages.append(text)
}
case .data(let data):
print("受信データ: \(data)")
@unknown default:
break
}
// 次のメッセージの受信
self?.receiveMessage()
case .failure(let error):
print("WebSocket受信エラー: \(error)")
DispatchQueue.main.async {
self?.connectionStatus = .disconnected
}
}
}
}
}
enum WebSocketConnectionStatus {
case disconnected
case connecting
case connected
}