URLSession
Standard HTTP client API for iOS/macOS provided by Apple. Part of Foundation framework, achieving native high-performance HTTP communication. Built-in async/await support, HTTP/2 compatibility, background transfer, detailed configuration options, and cookie management.
GitHub Overview
swiftlang/swift
The Swift Programming Language
Topics
Star History
Library
URLSession
Overview
URLSession is provided as part of the Swift Foundation framework as "a set of classes for configuring and managing network sessions." As the de facto standard HTTP client for Apple platforms, it supports network communication in iOS, macOS, watchOS, and tvOS applications. It enables asynchronous download and upload of data to and from endpoints identified by URLs, and comprehensively supports enterprise-level network features such as WebSocket communication, background downloads, and certificate pinning.
Details
URLSession 2025 edition provides modern asynchronous programming patterns through complete integration with Swift Concurrency (async/await), in addition to traditional callback-based APIs. With three main task types - URLSessionDataTask, URLSessionUploadTask, and URLSessionDownloadTask - it handles a wide range of network communication needs from basic HTTP requests to large file transfers. It provides advanced features that meet mobile application-specific requirements as standard, including background execution, App Transport Security (ATS), HTTP/2 support, and custom protocol support.
Key Features
- Comprehensive Task Types: Diverse communication patterns including Data, Upload, Download, and WebSocket
- Swift Concurrency Integration: Modern asynchronous programming with async/await
- Background Processing: Continuous data transfer when app is inactive
- Enhanced Security: ATS, certificate pinning, TLS 1.3 support
- HTTP/2 & HTTP/3 Support: High-speed communication with latest protocols
- Combine Framework Integration: Reactive programming pattern support
Pros and Cons
Pros
- Complete integration and optimization as Apple platform standard
- Excellent development experience through natural integration with Swift Concurrency
- Rich background download and mobile-specific features
- Standard compliance with security requirements for App Store review
- Advanced support for latest protocols like WebSocket and HTTP/3
- Memory management and performance optimization guaranteed by Apple
Cons
- Limited to Apple platforms, unavailable on other operating systems
- Complex for lightweight use cases requiring overly detailed configuration
- Limited network logging during debugging
- Constraints on customizability compared to third-party libraries
- Increased learning cost due to coexistence with legacy APIs
- Limited direct integration features with SwiftUI
Reference Pages
- URLSession Apple Official Documentation
- Networking with URLSession - Apple Developer
- Swift.org - Foundation
Code Examples
Basic Setup and Import
import Foundation
import Combine // When using Publisher
// async/await support for iOS 15+ / macOS 12+
@available(iOS 15.0, macOS 12.0, *)
class NetworkManager {
static let shared = NetworkManager()
private init() {}
// Basic URLSession instance
private let session = URLSession.shared
// Custom configured URLSession
private lazy var customSession: URLSession = {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
config.waitsForConnectivity = true
return URLSession(configuration: config)
}()
}
// Swift structures for responses
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?
}
Basic Requests (GET/POST/PUT/DELETE)
import Foundation
extension NetworkManager {
// MARK: - Async/Await API (iOS 15+)
// Basic GET request
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 request with query parameters
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("Response status: \(httpResponse.statusCode)")
print("Response headers: \(httpResponse.allHeaderFields)")
}
return try JSONDecoder().decode([User].self, from: data)
}
// POST request (sending 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 encode request body
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("User created: \(createdUser.name)")
return createdUser
} else {
// Handle error response
let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: data)
throw NSError(domain: "APIError", code: errorResponse.code, userInfo: [
NSLocalizedDescriptionKey: errorResponse.error
])
}
}
// PUT request (data update)
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")
// Convert Dictionary directly to 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 request
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("User deletion completed")
}
}
// MARK: - Traditional Callback-based API (iOS 13+)
extension NetworkManager {
// Callback-based request for legacy support
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()
}
}
Advanced Configuration and Customization (Authentication, Timeout, Cache, etc.)
import Foundation
import Network
class AdvancedNetworkManager {
// MARK: - Custom URLSessionConfiguration
static func createCustomSession() -> URLSession {
let config = URLSessionConfiguration.default
// Timeout settings
config.timeoutIntervalForRequest = 30.0
config.timeoutIntervalForResource = 120.0
// Connectivity settings
config.waitsForConnectivity = true
config.allowsCellularAccess = true
config.allowsExpensiveNetworkAccess = true
config.allowsConstrainedNetworkAccess = false
// Cache configuration
let cache = URLCache(
memoryCapacity: 50 * 1024 * 1024, // 50MB
diskCapacity: 200 * 1024 * 1024, // 200MB
diskPath: "networking_cache"
)
config.urlCache = cache
config.requestCachePolicy = .reloadIgnoringLocalCacheData
// Header configuration
config.httpAdditionalHeaders = [
"User-Agent": "MyApp/1.0 (iOS)",
"Accept-Language": "en-US,ja-JP",
"X-API-Version": "v2"
]
// HTTP configuration
config.httpMaximumConnectionsPerHost = 6
config.httpShouldUsePipelining = true
config.httpShouldSetCookies = true
config.httpCookieAcceptPolicy = .always
return URLSession(configuration: config)
}
// MARK: - Background Download Configuration
static func createBackgroundSession() -> URLSession {
let config = URLSessionConfiguration.background(
withIdentifier: "com.myapp.background-download"
)
// Background-specific settings
config.isDiscretionary = true // Enable system optimization
config.sessionSendsLaunchEvents = true
// Connection settings
config.waitsForConnectivity = true
config.allowsCellularAccess = false // WiFi only
return URLSession(configuration: config, delegate: nil, delegateQueue: nil)
}
// MARK: - Authentication Features
class AuthenticationManager: NSObject, URLSessionDelegate {
// HTTP Basic Authentication
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 Certificate Pinning
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
}
// Custom validation for certificate pinning
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: - Custom HTTP Header Management
func createAuthenticatedRequest(url: URL, token: String) -> URLRequest {
var request = URLRequest(url: url)
// Authentication headers
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
// Custom headers
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")
// Cache control
request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
return request
}
// MARK: - Proxy Configuration (iOS 14+)
@available(iOS 14.0, *)
func configureProxySettings() -> URLSessionConfiguration {
let config = URLSessionConfiguration.default
// HTTP proxy settings
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
}
}
Error Handling and Retry Functionality
import Foundation
import os.log
// Custom error definitions
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 "Invalid URL"
case .noData:
return "No data received"
case .decodingError(let error):
return "Data parsing failed: \(error.localizedDescription)"
case .serverError(let code, let message):
return "Server error (\(code)): \(message ?? "Unknown error")"
case .networkUnavailable:
return "Network unavailable"
case .timeout:
return "Request timed out"
case .unauthorized:
return "Authentication required"
case .rateLimited(let retryAfter):
let after = retryAfter.map { String(format: "%.0f seconds", $0) } ?? "a while"
return "Rate limited. Please retry after \(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: - Request with Retry Functionality
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, "Invalid response")
}
// Status code handling
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
// Non-retryable errors
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
// Retry on network errors
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: - Comprehensive Error Handling
func handleResponse<T: Codable>(
data: Data,
response: URLResponse,
type: T.Type
) throws -> T {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.serverError(0, "Not an HTTP response")
}
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, "Resource not found")
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, "Internal server error")
default:
throw NetworkError.serverError(httpResponse.statusCode, "Unexpected error")
}
}
// MARK: - Network Connectivity Monitoring
@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)
}
}
}
// Usage example
@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 = "Unexpected error: \(error.localizedDescription)"
}
isLoading = false
}
}
File Operations and Upload/Download
import Foundation
class FileTransferManager {
private let session: URLSession
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForResource = 600 // 10 minutes
self.session = URLSession(configuration: config)
}
// MARK: - File Download
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)
}
// Move temporary file to destination
try FileManager.default.moveItem(at: tempURL, to: destinationURL)
print("File download completed: \(destinationURL.path)")
}
// Download with progress (using 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
}
// Final progress
continuation.yield(DownloadProgress(
bytesWritten: httpResponse.expectedContentLength,
totalBytes: httpResponse.expectedContentLength,
progress: 1.0,
tempURL: tempURL
))
continuation.finish()
}
task.resume()
}
}
// MARK: - File Upload
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)
}
// Multipart form data upload
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()
// Add field 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)!)
}
// Add file data
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: - Background Download
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
}
}
// Related type definitions
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 usage example
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("Downloading: \(Int(downloadProgress * 100))%")
} else {
Button("Download File") {
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("Download error: \(error)")
}
isDownloading = false
}
}
Combine Framework Integration and Practical Usage Examples
import Foundation
import Combine
import SwiftUI
// MARK: - Combine Publisher Extensions
extension URLSession {
// Generic API request 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, "Invalid response")
}
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-based API Client
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?
// Fetch user list
func fetchUsers() {
guard let url = URL(string: "https://api.example.com/users") else {
errorMessage = "Invalid 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)
}
// Reactive user search
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()
}
// Batch request processing
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("Fetched user details: \(validUsers.count) items")
}
.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 Integration
struct UserListView: View {
@StateObject private var apiClient = CombineAPIClient()
@State private var searchText = ""
@State private var searchResults: [User] = []
var body: some View {
NavigationView {
VStack {
// Search bar
SearchBar(text: $searchText)
.onChange(of: searchText) { query in
searchUsers(query: query)
}
// User list
if apiClient.isLoading {
ProgressView("Loading...")
} 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("User List")
.onAppear {
apiClient.fetchUsers()
}
.alert("Error", 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) years old")
.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("Search users...", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding(.horizontal)
}
}
// MARK: - WebSocket Communication Example
@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
// Start receiving messages
receiveMessage()
// Monitor connection status
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 send error: \(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("Received data: \(data)")
@unknown default:
break
}
// Receive next message
self?.receiveMessage()
case .failure(let error):
print("WebSocket receive error: \(error)")
DispatchQueue.main.async {
self?.connectionStatus = .disconnected
}
}
}
}
}
enum WebSocketConnectionStatus {
case disconnected
case connecting
case connected
}