# Networking URLSession patterns for API calls, authentication, caching, and offline support. ```swift actor NetworkService { private let session: URLSession private let decoder: JSONDecoder init(session: URLSession = .shared) { self.session = session self.decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 } func fetch(_ request: URLRequest) async throws -> T { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw NetworkError.invalidResponse } guard 200..<300 ~= httpResponse.statusCode else { throw NetworkError.httpError(httpResponse.statusCode, data) } return try decoder.decode(T.self, from: data) } func fetchData(_ request: URLRequest) async throws -> Data { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { throw NetworkError.requestFailed } return data } } enum NetworkError: Error { case invalidResponse case httpError(Int, Data) case requestFailed case decodingError(Error) } ``` ```swift struct Endpoint { let path: String let method: HTTPMethod let queryItems: [URLQueryItem]? let body: Data? let headers: [String: String]? enum HTTPMethod: String { case get = "GET" case post = "POST" case put = "PUT" case patch = "PATCH" case delete = "DELETE" } var request: URLRequest { var components = URLComponents() components.scheme = "https" components.host = "api.example.com" components.path = path components.queryItems = queryItems var request = URLRequest(url: components.url!) request.httpMethod = method.rawValue request.httpBody = body // Default headers request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") // Custom headers headers?.forEach { request.setValue($1, forHTTPHeaderField: $0) } return request } } // Usage extension Endpoint { static func projects() -> Endpoint { Endpoint(path: "/v1/projects", method: .get, queryItems: nil, body: nil, headers: nil) } static func project(id: UUID) -> Endpoint { Endpoint(path: "/v1/projects/\(id)", method: .get, queryItems: nil, body: nil, headers: nil) } static func createProject(_ project: CreateProjectRequest) -> Endpoint { let body = try? JSONEncoder().encode(project) return Endpoint(path: "/v1/projects", method: .post, queryItems: nil, body: body, headers: nil) } } ``` ```swift actor AuthenticatedNetworkService { private let session: URLSession private var token: String? init() { let config = URLSessionConfiguration.default config.httpAdditionalHeaders = [ "User-Agent": "MyApp/1.0" ] self.session = URLSession(configuration: config) } func setToken(_ token: String) { self.token = token } func fetch(_ endpoint: Endpoint) async throws -> T { var request = endpoint.request if let token = token { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw NetworkError.invalidResponse } if httpResponse.statusCode == 401 { throw NetworkError.unauthorized } guard 200..<300 ~= httpResponse.statusCode else { throw NetworkError.httpError(httpResponse.statusCode, data) } return try JSONDecoder().decode(T.self, from: data) } } ``` ```swift actor OAuthService { private var accessToken: String? private var refreshToken: String? private var tokenExpiry: Date? private var isRefreshing = false func validToken() async throws -> String { // Return existing valid token if let token = accessToken, let expiry = tokenExpiry, expiry > Date().addingTimeInterval(60) { return token } // Refresh if needed return try await refreshAccessToken() } private func refreshAccessToken() async throws -> String { guard !isRefreshing else { // Wait for in-progress refresh try await Task.sleep(for: .milliseconds(100)) return try await validToken() } isRefreshing = true defer { isRefreshing = false } guard let refresh = refreshToken else { throw AuthError.noRefreshToken } let request = Endpoint.refreshToken(refresh).request let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(TokenResponse.self, from: data) accessToken = response.accessToken refreshToken = response.refreshToken tokenExpiry = Date().addingTimeInterval(TimeInterval(response.expiresIn)) // Save to keychain try saveTokens() return response.accessToken } } ``` ```swift // Configure cache in URLSession let config = URLSessionConfiguration.default config.urlCache = URLCache( memoryCapacity: 50 * 1024 * 1024, // 50 MB memory diskCapacity: 100 * 1024 * 1024, // 100 MB disk diskPath: "network_cache" ) config.requestCachePolicy = .returnCacheDataElseLoad let session = URLSession(configuration: config) ``` ```swift actor ResponseCache { private var cache: [String: CachedResponse] = [:] private let maxAge: TimeInterval init(maxAge: TimeInterval = 300) { // 5 minutes default self.maxAge = maxAge } func get(_ key: String) -> T? { guard let cached = cache[key], Date().timeIntervalSince(cached.timestamp) < maxAge else { cache[key] = nil return nil } return try? JSONDecoder().decode(T.self, from: cached.data) } func set(_ value: T, for key: String) { guard let data = try? JSONEncoder().encode(value) else { return } cache[key] = CachedResponse(data: data, timestamp: Date()) } func invalidate(_ key: String) { cache[key] = nil } func clear() { cache.removeAll() } } struct CachedResponse { let data: Data let timestamp: Date } // Usage actor CachedNetworkService { private let network: NetworkService private let cache = ResponseCache() func fetchProjects(forceRefresh: Bool = false) async throws -> [Project] { let cacheKey = "projects" if !forceRefresh, let cached: [Project] = await cache.get(cacheKey) { return cached } let projects: [Project] = try await network.fetch(Endpoint.projects().request) await cache.set(projects, for: cacheKey) return projects } } ``` ```swift @Observable class OfflineAwareService { private let network: NetworkService private let storage: LocalStorage var isOnline = true init(network: NetworkService, storage: LocalStorage) { self.network = network self.storage = storage monitorConnectivity() } func fetchProjects() async throws -> [Project] { if isOnline { do { let projects = try await network.fetch(Endpoint.projects().request) try storage.save(projects, for: "projects") return projects } catch { // Fall back to cache on network error if let cached = try? storage.load("projects") as [Project] { return cached } throw error } } else { // Offline: use cache guard let cached = try? storage.load("projects") as [Project] else { throw NetworkError.offline } return cached } } private func monitorConnectivity() { let monitor = NWPathMonitor() monitor.pathUpdateHandler = { [weak self] path in Task { @MainActor in self?.isOnline = path.status == .satisfied } } monitor.start(queue: .global()) } } ``` ```swift actor UploadService { func upload(file: URL, to endpoint: Endpoint) async throws -> UploadResponse { var request = endpoint.request let boundary = UUID().uuidString request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") let data = try Data(contentsOf: file) let body = createMultipartBody( data: data, filename: file.lastPathComponent, boundary: boundary ) request.httpBody = body let (responseData, _) = try await URLSession.shared.data(for: request) return try JSONDecoder().decode(UploadResponse.self, from: responseData) } private func createMultipartBody(data: Data, filename: String, boundary: String) -> Data { var body = Data() body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!) body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!) body.append(data) body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) return body } } ``` ```swift actor DownloadService { func download(from url: URL, to destination: URL) async throws { let (tempURL, response) = try await URLSession.shared.download(from: url) guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { throw NetworkError.downloadFailed } // Move to destination let fileManager = FileManager.default if fileManager.fileExists(atPath: destination.path) { try fileManager.removeItem(at: destination) } try fileManager.moveItem(at: tempURL, to: destination) } func downloadWithProgress(from url: URL) -> AsyncThrowingStream { AsyncThrowingStream { continuation in let task = URLSession.shared.downloadTask(with: url) { tempURL, response, error in if let error = error { continuation.finish(throwing: error) return } guard let tempURL = tempURL else { continuation.finish(throwing: NetworkError.downloadFailed) return } continuation.yield(.completed(tempURL)) continuation.finish() } // Observe progress let observation = task.progress.observe(\.fractionCompleted) { progress, _ in continuation.yield(.progress(progress.fractionCompleted)) } continuation.onTermination = { _ in observation.invalidate() task.cancel() } task.resume() } } } enum DownloadProgress { case progress(Double) case completed(URL) } ``` ```swift enum NetworkError: LocalizedError { case invalidResponse case httpError(Int, Data) case unauthorized case offline case timeout case decodingError(Error) var errorDescription: String? { switch self { case .invalidResponse: return "Invalid server response" case .httpError(let code, _): return "Server error: \(code)" case .unauthorized: return "Authentication required" case .offline: return "No internet connection" case .timeout: return "Request timed out" case .decodingError(let error): return "Data error: \(error.localizedDescription)" } } var isRetryable: Bool { switch self { case .httpError(let code, _): return code >= 500 case .timeout, .offline: return true default: return false } } } // Retry logic func fetchWithRetry( _ request: URLRequest, maxAttempts: Int = 3 ) async throws -> T { var lastError: Error? for attempt in 1...maxAttempts { do { return try await network.fetch(request) } catch let error as NetworkError where error.isRetryable { lastError = error let delay = pow(2.0, Double(attempt - 1)) // Exponential backoff try await Task.sleep(for: .seconds(delay)) } catch { throw error } } throw lastError ?? NetworkError.requestFailed } ``` ```swift // Mock URLProtocol for testing class MockURLProtocol: URLProtocol { static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { fatalError("Handler not set") } do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } override func stopLoading() {} } // Test setup func testFetchProjects() async throws { let config = URLSessionConfiguration.ephemeral config.protocolClasses = [MockURLProtocol.self] let session = URLSession(configuration: config) MockURLProtocol.requestHandler = { request in let response = HTTPURLResponse( url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil )! let data = try JSONEncoder().encode([Project(name: "Test")]) return (response, data) } let service = NetworkService(session: session) let projects: [Project] = try await service.fetch(Endpoint.projects().request) XCTAssertEqual(projects.count, 1) } ```