# Networking URLSession patterns, caching, authentication, and offline support. ## Basic Networking Service ```swift actor NetworkService { private let session: URLSession private let decoder: JSONDecoder private let encoder: JSONEncoder init(session: URLSession = .shared) { self.session = session self.decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 decoder.keyDecodingStrategy = .convertFromSnakeCase self.encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.keyEncodingStrategy = .convertToSnakeCase } func fetch(_ endpoint: Endpoint) async throws -> T { let request = try endpoint.urlRequest() 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) } do { return try decoder.decode(T.self, from: data) } catch { throw NetworkError.decodingError(error) } } func send(_ body: T, to endpoint: Endpoint) async throws -> R { var request = try endpoint.urlRequest() request.httpBody = try encoder.encode(body) request.setValue("application/json", forHTTPHeaderField: "Content-Type") 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(R.self, from: data) } } enum NetworkError: LocalizedError { case invalidURL case invalidResponse case httpError(Int, Data) case decodingError(Error) case noConnection case timeout var errorDescription: String? { switch self { case .invalidURL: return "Invalid URL" case .invalidResponse: return "Invalid server response" case .httpError(let code, _): return "Server error (\(code))" case .decodingError: return "Failed to parse response" case .noConnection: return "No internet connection" case .timeout: return "Request timed out" } } } ``` ## Endpoint Pattern ```swift enum Endpoint { case items case item(id: String) case createItem case updateItem(id: String) case deleteItem(id: String) case search(query: String, page: Int) var baseURL: URL { URL(string: "https://api.example.com/v1")! } var path: String { switch self { case .items, .createItem: return "/items" case .item(let id), .updateItem(let id), .deleteItem(let id): return "/items/\(id)" case .search: return "/search" } } var method: String { switch self { case .items, .item, .search: return "GET" case .createItem: return "POST" case .updateItem: return "PUT" case .deleteItem: return "DELETE" } } var queryItems: [URLQueryItem]? { switch self { case .search(let query, let page): return [ URLQueryItem(name: "q", value: query), URLQueryItem(name: "page", value: String(page)) ] default: return nil } } func urlRequest() throws -> URLRequest { var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true) components?.queryItems = queryItems guard let url = components?.url else { throw NetworkError.invalidURL } var request = URLRequest(url: url) request.httpMethod = method request.setValue("application/json", forHTTPHeaderField: "Accept") return request } } ``` ## Authentication ### Bearer Token ```swift actor AuthenticatedNetworkService { private let session: URLSession private let tokenProvider: TokenProvider init(tokenProvider: TokenProvider) { self.session = .shared self.tokenProvider = tokenProvider } func fetch(_ endpoint: Endpoint) async throws -> T { var request = try endpoint.urlRequest() // Add auth header let token = try await tokenProvider.validToken() 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 } // Handle 401 - token expired if httpResponse.statusCode == 401 { // Refresh token and retry let newToken = try await tokenProvider.refreshToken() request.setValue("Bearer \(newToken)", forHTTPHeaderField: "Authorization") let (retryData, retryResponse) = try await session.data(for: request) guard let retryHttpResponse = retryResponse as? HTTPURLResponse, 200..<300 ~= retryHttpResponse.statusCode else { throw NetworkError.unauthorized } return try JSONDecoder().decode(T.self, from: retryData) } guard 200..<300 ~= httpResponse.statusCode else { throw NetworkError.httpError(httpResponse.statusCode, data) } return try JSONDecoder().decode(T.self, from: data) } } protocol TokenProvider { func validToken() async throws -> String func refreshToken() async throws -> String } ``` ### OAuth 2.0 Flow ```swift import AuthenticationServices class OAuthService: NSObject { func signIn() async throws -> String { let authURL = URL(string: "https://auth.example.com/authorize?client_id=xxx&redirect_uri=myapp://callback&response_type=code")! return try await withCheckedThrowingContinuation { continuation in let session = ASWebAuthenticationSession( url: authURL, callbackURLScheme: "myapp" ) { callbackURL, error in if let error = error { continuation.resume(throwing: error) return } guard let callbackURL = callbackURL, let code = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)? .queryItems?.first(where: { $0.name == "code" })?.value else { continuation.resume(throwing: OAuthError.invalidCallback) return } continuation.resume(returning: code) } session.presentationContextProvider = self session.prefersEphemeralWebBrowserSession = true session.start() } } } extension OAuthService: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap { $0.windows } .first { $0.isKeyWindow }! } } ``` ## Caching ### URLCache Configuration ```swift class CachedNetworkService { private let session: URLSession init() { let cache = URLCache( memoryCapacity: 50 * 1024 * 1024, // 50 MB memory diskCapacity: 200 * 1024 * 1024 // 200 MB disk ) let config = URLSessionConfiguration.default config.urlCache = cache config.requestCachePolicy = .returnCacheDataElseLoad self.session = URLSession(configuration: config) } func fetch(_ endpoint: Endpoint, cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy) async throws -> T { var request = try endpoint.urlRequest() request.cachePolicy = cachePolicy let (data, _) = try await session.data(for: request) return try JSONDecoder().decode(T.self, from: data) } func fetchFresh(_ endpoint: Endpoint) async throws -> T { try await fetch(endpoint, cachePolicy: .reloadIgnoringLocalCacheData) } } ``` ### Custom Caching ```swift actor DataCache { private var cache: [String: CachedItem] = [:] private let maxAge: TimeInterval struct CachedItem { let data: Data let timestamp: Date } init(maxAge: TimeInterval = 300) { self.maxAge = maxAge } func get(_ key: String) -> Data? { guard let item = cache[key] else { return nil } guard Date().timeIntervalSince(item.timestamp) < maxAge else { cache.removeValue(forKey: key) return nil } return item.data } func set(_ data: Data, for key: String) { cache[key] = CachedItem(data: data, timestamp: Date()) } func invalidate(_ key: String) { cache.removeValue(forKey: key) } func clearAll() { cache.removeAll() } } ``` ## Offline Support ### Network Monitor ```swift import Network @Observable class NetworkMonitor { var isConnected = true var connectionType: ConnectionType = .wifi private let monitor = NWPathMonitor() private let queue = DispatchQueue(label: "NetworkMonitor") enum ConnectionType { case wifi, cellular, unknown } init() { monitor.pathUpdateHandler = { [weak self] path in DispatchQueue.main.async { self?.isConnected = path.status == .satisfied self?.connectionType = self?.getConnectionType(path) ?? .unknown } } monitor.start(queue: queue) } private func getConnectionType(_ path: NWPath) -> ConnectionType { if path.usesInterfaceType(.wifi) { return .wifi } else if path.usesInterfaceType(.cellular) { return .cellular } return .unknown } deinit { monitor.cancel() } } ``` ### Offline-First Pattern ```swift actor OfflineFirstService { private let network: NetworkService private let storage: StorageService private let cache: DataCache func fetchItems() async throws -> [Item] { // Try cache first if let cached = await cache.get("items"), let items = try? JSONDecoder().decode([Item].self, from: cached) { // Return cached, fetch fresh in background Task { try? await fetchAndCacheFresh() } return items } // Try network do { let items: [Item] = try await network.fetch(.items) await cache.set(try JSONEncoder().encode(items), for: "items") return items } catch { // Fall back to storage return try await storage.loadItems() } } private func fetchAndCacheFresh() async throws { let items: [Item] = try await network.fetch(.items) await cache.set(try JSONEncoder().encode(items), for: "items") try await storage.saveItems(items) } } ``` ### Pending Operations Queue ```swift actor PendingOperationsQueue { private var operations: [PendingOperation] = [] private let storage: StorageService struct PendingOperation: Codable { let id: UUID let endpoint: String let method: String let body: Data? let createdAt: Date } func add(_ operation: PendingOperation) async { operations.append(operation) try? await persist() } func processAll() async { for operation in operations { do { try await execute(operation) operations.removeAll { $0.id == operation.id } } catch { // Keep in queue for retry continue } } try? await persist() } private func execute(_ operation: PendingOperation) async throws { // Execute network request } private func persist() async throws { try await storage.savePendingOperations(operations) } } ``` ## Multipart Upload ```swift extension NetworkService { func upload(_ fileData: Data, filename: String, mimeType: String, to endpoint: Endpoint) async throws -> UploadResponse { let boundary = UUID().uuidString var request = try endpoint.urlRequest() request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 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: \(mimeType)\r\n\r\n".data(using: .utf8)!) body.append(fileData) body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) request.httpBody = body let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { throw NetworkError.httpError((response as? HTTPURLResponse)?.statusCode ?? 0, data) } return try JSONDecoder().decode(UploadResponse.self, from: data) } } ``` ## Download with Progress ```swift class DownloadService: NSObject, URLSessionDownloadDelegate { private lazy var session: URLSession = { URLSession(configuration: .default, delegate: self, delegateQueue: nil) }() private var progressHandler: ((Double) -> Void)? private var completionHandler: ((Result) -> Void)? func download(from url: URL, progress: @escaping (Double) -> Void) async throws -> URL { try await withCheckedThrowingContinuation { continuation in self.progressHandler = progress self.completionHandler = { result in continuation.resume(with: result) } session.downloadTask(with: url).resume() } } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { completionHandler?(.success(location)) } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) DispatchQueue.main.async { self.progressHandler?(progress) } } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error = error { completionHandler?(.failure(error)) } } } ```