The Ultimate Guide to API Calling in Swift 5: From TCP/UDP to Advanced Networking

 

The Ultimate Guide to API Calling in Swift 5: From TCP/UDP to Advanced Networking


API calling is the backbone of modern iOS applications. Whether you're building a social media app, e-commerce platform, or data-driven application, understanding how to effectively communicate with remote servers is crucial. This comprehensive guide covers everything from networking fundamentals to advanced API implementation techniques in Swift 5.

Table of Contents

  1. Networking Fundamentals
  2. TCP vs UDP
  3. HTTP Methods Deep Dive
  4. URLSession Architecture
  5. GET Requests
  6. POST Requests
  7. PUT Requests
  8. DELETE Requests
  9. Advanced Networking
  10. Error Handling
  11. Authentication
  12. Performance Optimisation
  13. Testing API Calls
  14. Best Practices

Networking Fundamentals

Understanding the Network Stack

Before diving into Swift-specific implementations, it's essential to understand the underlying network protocols that power API communication.

The OSI Model in Context

The Open Systems Interconnection (OSI) model provides a conceptual framework for understanding network communication:

  1. Physical Layer: Hardware transmission (cables, wireless)
  2. Data Link Layer: Node-to-node delivery (Ethernet, Wi-Fi)
  3. Network Layer: Routing (IP)
  4. Transport Layer: End-to-end communication (TCP, UDP)
  5. Session Layer: Managing connections
  6. Presentation Layer: Data formatting (encryption, compression)
  7. Application Layer: User interface (HTTP, HTTPS, FTP)

For iOS developers, we primarily work with layers 4-7, with HTTP/HTTPS at the application layer being our main focus.

Internet Protocol Suite

The Internet Protocol Suite (TCP/IP) is the foundation of internet communication:

  • Internet Protocol (IP): Delivers packets from source to destination
  • Transmission Control Protocol (TCP): Reliable, connection-oriented protocol
  • User Datagram Protocol (UDP): Fast, connectionless protocol
  • Internet Control Message Protocol (ICMP): Error reporting and diagnostics

TCP vs UDP

Understanding the differences between TCP and UDP is crucial for choosing the right protocol for your application needs.

TCP (Transmission Control Protocol)

TCP is a connection-oriented, reliable protocol that ensures data integrity and ordered delivery.

TCP Characteristics:

  • Connection-Oriented: Establishes a connection before data transfer
  • Reliable: Guarantees data delivery and order
  • Error Detection: Built-in error checking and correction
  • Flow Control: Manages data transmission rate
  • Congestion Control: Adapts to network conditions

When to Use TCP:

  • Web browsing (HTTP/HTTPS)
  • File transfers (FTP)
  • Email (SMTP, POP3, IMAP)
  • Remote login (SSH, Telnet)
  • Most REST APIs

TCP Connection Process (Three-Way Handshake):

  1. SYN: Client sends synchronization request
  2. SYN-ACK: Server acknowledges and sends synchronization
  3. ACK: Client acknowledges server's synchronization

UDP (User Datagram Protocol)

UDP is a connectionless, fast protocol that prioritizes speed over reliability.

UDP Characteristics:

  • Connectionless: No connection establishment required
  • Fast: Lower overhead, faster transmission
  • Unreliable: No guarantee of delivery or order
  • No Flow Control: Sends data at maximum rate
  • Stateless: Each packet is independent

When to Use UDP:

  • Real-time gaming
  • Video streaming
  • DNS lookups
  • Live broadcasts
  • IoT sensor data
  • Voice over IP (VoIP)

Comparison Table

Feature  TCP  UDP
Connection  Connection-oriented  Connectionless
Reliability  Reliable  Unreliable
Speed  Slower  Faster
Overhead  High  Low
Error Checking  Yes  Basic
Use Cases  Web APIs, File Transfer  Gaming, Streaming

HTTP Methods Deep Dive

HTTP (HyperText Transfer Protocol) defines several methods for different types of operations. Understanding each method's purpose and proper usage is essential for RESTful API design.

HTTP Method Categories

Safe Methods

Methods that don't modify server state:

  • GET: Retrieve data
  • HEAD: Retrieve headers only
  • OPTIONS: Get allowed methods

Idempotent Methods

Methods that produce the same result regardless of how many times they're executed:

  • GET, PUT, DELETE, HEAD, OPTIONS

Non-Idempotent Methods

Methods that may produce different results on repeated calls:

  • POST

Detailed HTTP Methods

GET - Retrieve Data

  • Purpose: Retrieve data from server
  • Safe: Yes (read-only)
  • Idempotent: Yes
  • Cacheable: Yes
  • Body: No request body
  • Response: Contains requested data

POST - Create New Resources

  • Purpose: Submit data to create new resources
  • Safe: No (modifies server state)
  • Idempotent: No
  • Cacheable: No
  • Body: Contains data to be processed
  • Response: Usually contains created resource

PUT - Update/Replace Resources

  • Purpose: Update entire resource or create if doesn't exist
  • Safe: No (modifies server state)
  • Idempotent: Yes
  • Cacheable: No
  • Body: Contains complete resource data
  • Response: Updated resource or confirmation

DELETE - Remove Resources

  • Purpose: Remove specified resource
  • Safe: No (modifies server state)
  • Idempotent: Yes
  • Cacheable: No
  • Body: Usually no body
  • Response: Confirmation or deleted resource

PATCH - Partial Updates

  • Purpose: Apply partial modifications
  • Safe: No
  • Idempotent: No (generally)
  • Cacheable: No
  • Body: Contains partial update data
  • Response: Updated resource

HEAD - Retrieve Headers

  • Purpose: Get response headers without body
  • Safe: Yes
  • Idempotent: Yes
  • Cacheable: Yes
  • Body: No request body
  • Response: Headers only, no body

OPTIONS - Discover Capabilities

  • Purpose: Get allowed methods and capabilities
  • Safe: Yes
  • Idempotent: Yes
  • Cacheable: Yes
  • Body: Usually no body
  • Response: Allowed methods and options

URLSession Architecture

URLSession is Apple's powerful networking API that provides a rich set of delegate methods for handling uploads, downloads, and data tasks in the background.

URLSession Components

URLSession

The main class that manages network requests. Different session types:

  • Shared Session: URLSession.shared - Simple requests
  • Default Session: Custom configuration, similar to shared
  • Ephemeral Session: No persistent storage (private browsing)
  • Background Session: Background downloads/uploads

URLSessionConfiguration

Configures session behavior:

  • Timeout intervals
  • Cache policies
  • Cookie handling
  • Connection requirements
  • Protocol support

URLSessionTask Types

  1. URLSessionDataTask: In-memory requests/responses
  2. URLSessionUploadTask: Upload files to servers
  3. URLSessionDownloadTask: Download files to disk
  4. URLSessionStreamTask: TCP/IP connections
  5. URLSessionWebSocketTask: WebSocket connections (iOS 13+)

URLRequest

Represents a URL load request with:

  • URL: Target endpoint
  • HTTP Method: GET, POST, etc.
  • Headers: Request metadata
  • Body: Request payload
  • Cache Policy: Caching behavior
  • Timeout: Request timeout

Session Lifecycle

  1. Create URLSession with appropriate configuration
  2. Create URLRequest with endpoint details
  3. Create URLSessionTask from session and request
  4. Start task using resume()
  5. Handle response in completion handler or delegate
  6. Process data and update UI
  7. Clean up resources

Thanks for reading this long,

here is one advance tip for you,

When building advanced API calling for video call or livestream in Swift 5, always use WebRTC for real-time peer-to-peer communication combined with URLSessionWebSocketTask or a dedicated signalling server (via REST + WebSockets) to exchange session descriptions and ICE candidates; implement adaptive bitrate streaming (HLS or RTMP fallback) to handle varying network conditions; use background tasks and QoS priorities to keep streaming alive without blocking UI; secure API calls with JWT or OAuth 2.0 tokens to protect streams; add retry logic with exponential backoff for signalling and media endpoints; and profile with Instruments (Network + Energy) to ensure low latency and battery efficiency—this combination ensures stable, secure, and scalable real-time video communication.


GET Requests

GET requests are the most common HTTP method, used to retrieve data from servers. They should be safe (read-only) and idempotent.

Basic GET Implementation

import Foundation

class APIManager {
    static let shared = APIManager()
    private let session = URLSession.shared
    private let baseURL = "https://api.example.com"
    
    private init() {}
    
    func fetchUsers(completion: @escaping (Result<[User], APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        let task = session.dataTask(with: request) { data, response, error in
            DispatchQueue.main.async {
                if let error = error {
                    completion(.failure(.networkError(error)))
                    return
                }
                
                guard let httpResponse = response as? HTTPURLResponse else {
                    completion(.failure(.invalidResponse))
                    return
                }
                
                guard 200...299 ~= httpResponse.statusCode else {
                    completion(.failure(.serverError(httpResponse.statusCode)))
                    return
                }
                
                guard let data = data else {
                    completion(.failure(.noData))
                    return
                }
                
                do {
                    let users = try JSONDecoder().decode([User].self, from: data)
                    completion(.success(users))
                } catch {
                    completion(.failure(.decodingError(error)))
                }
            }
        }
        
        task.resume()
    }
}

Advanced GET with Query Parameters

extension APIManager {
    func fetchUsers(page: Int, limit: Int, search: String?, completion: @escaping (Result<UserResponse, APIError>) -> Void) {
        var components = URLComponents(string: "\(baseURL)/users")!
        
        var queryItems = [
            URLQueryItem(name: "page", value: String(page)),
            URLQueryItem(name: "limit", value: String(limit))
        ]
        
        if let search = search, !search.isEmpty {
            queryItems.append(URLQueryItem(name: "search", value: search))
        }
        
        components.queryItems = queryItems
        
        guard let url = components.url else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
        
        performDataTask(request: request, completion: completion)
    }
}

GET with Custom Headers

extension APIManager {
    func fetchUserProfile(userID: String, completion: @escaping (Result<UserProfile, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users/\(userID)") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        
        // Custom headers
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
        request.setValue("iOS", forHTTPHeaderField: "X-Client-Platform")
        request.setValue(Bundle.main.appVersion, forHTTPHeaderField: "X-Client-Version")
        request.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-ID")
        
        // Cache policy
        request.cachePolicy = .returnCacheDataElseLoad
        request.timeoutInterval = 30
        
        performDataTask(request: request, completion: completion)
    }
}

POST Requests

POST requests are used to submit data to servers, typically to create new resources. They are neither safe nor idempotent.

Basic POST Implementation

extension APIManager {
    func createUser(_ user: CreateUserRequest, completion: @escaping (Result<User, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        do {
            let jsonData = try JSONEncoder().encode(user)
            request.httpBody = jsonData
        } catch {
            completion(.failure(.encodingError(error)))
            return
        }
        
        performDataTask(request: request, completion: completion)
    }
}

POST with Form Data

extension APIManager {
    func uploadUserAvatar(userID: String, image: UIImage, completion: @escaping (Result<UploadResponse, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users/\(userID)/avatar") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        
        let boundary = UUID().uuidString
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
        
        // Create multipart form data
        var formData = Data()
        
        // Add image data
        if let imageData = image.jpegData(compressionQuality: 0.8) {
            formData.append("--\(boundary)\r\n".data(using: .utf8)!)
            formData.append("Content-Disposition: form-data; name=\"avatar\"; filename=\"avatar.jpg\"\r\n".data(using: .utf8)!)
            formData.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
            formData.append(imageData)
            formData.append("\r\n".data(using: .utf8)!)
        }
        
        // Add user ID
        formData.append("--\(boundary)\r\n".data(using: .utf8)!)
        formData.append("Content-Disposition: form-data; name=\"user_id\"\r\n\r\n".data(using: .utf8)!)
        formData.append(userID.data(using: .utf8)!)
        formData.append("\r\n".data(using: .utf8)!)
        
        // Close boundary
        formData.append("--\(boundary)--\r\n".data(using: .utf8)!)
        
        request.httpBody = formData
        
        performDataTask(request: request, completion: completion)
    }
}

POST with URL Encoded Data

extension APIManager {
    func loginUser(email: String, password: String, completion: @escaping (Result<AuthResponse, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/auth/login") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        
        // Create URL encoded body
        var components = URLComponents()
        components.queryItems = [
            URLQueryItem(name: "email", value: email),
            URLQueryItem(name: "password", value: password),
            URLQueryItem(name: "grant_type", value: "password"),
            URLQueryItem(name: "client_id", value: "ios_app")
        ]
        
        request.httpBody = components.query?.data(using: .utf8)
        
        performDataTask(request: request, completion: completion)
    }
}

PUT Requests

PUT requests are used to update entire resources or create resources if they don't exist. They should be idempotent.

Basic PUT Implementation

extension APIManager {
    func updateUser(_ user: User, completion: @escaping (Result<User, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users/\(user.id)") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "PUT"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
        
        do {
            let jsonData = try JSONEncoder().encode(user)
            request.httpBody = jsonData
        } catch {
            completion(.failure(.encodingError(error)))
            return
        }
        
        performDataTask(request: request, completion: completion)
    }
}

PUT for Resource Creation

extension APIManager {
    func createOrUpdateUserSettings(_ settings: UserSettings, userID: String, completion: @escaping (Result<UserSettings, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users/\(userID)/settings") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "PUT"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
        
        // Add conditional headers
        if let lastModified = settings.lastModified {
            let formatter = DateFormatter()
            formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
            request.setValue(formatter.string(from: lastModified), forHTTPHeaderField: "If-Unmodified-Since")
        }
        
        do {
            let encoder = JSONEncoder()
            encoder.dateEncodingStrategy = .iso8601
            let jsonData = try encoder.encode(settings)
            request.httpBody = jsonData
        } catch {
            completion(.failure(.encodingError(error)))
            return
        }
        
        performDataTask(request: request, completion: completion)
    }
}

DELETE Requests

DELETE requests remove resources from the server. They should be idempotent - multiple DELETE requests for the same resource should have the same effect.

Basic DELETE Implementation

extension APIManager {
    func deleteUser(userID: String, completion: @escaping (Result<Void, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users/\(userID)") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "DELETE"
        request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
        
        let task = session.dataTask(with: request) { data, response, error in
            DispatchQueue.main.async {
                if let error = error {
                    completion(.failure(.networkError(error)))
                    return
                }
                
                guard let httpResponse = response as? HTTPURLResponse else {
                    completion(.failure(.invalidResponse))
                    return
                }
                
                switch httpResponse.statusCode {
                case 200, 204:
                    completion(.success(()))
                case 404:
                    completion(.failure(.resourceNotFound))
                case 403:
                    completion(.failure(.forbidden))
                default:
                    completion(.failure(.serverError(httpResponse.statusCode)))
                }
            }
        }
        
        task.resume()
    }
}

DELETE with Confirmation

extension APIManager {
    func deleteUserAccount(userID: String, password: String, completion: @escaping (Result<DeletionResponse, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users/\(userID)") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "DELETE"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
        
        // Add confirmation data
        let confirmationData = ["password": password, "confirm_deletion": true] as [String: Any]
        
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: confirmationData)
            request.httpBody = jsonData
        } catch {
            completion(.failure(.encodingError(error)))
            return
        }
        
        performDataTask(request: request, completion: completion)
    }
}

Bulk DELETE Operations

extension APIManager {
    func deleteMultipleUsers(userIDs: [String], completion: @escaping (Result<BulkDeleteResponse, APIError>) -> Void) {
        guard let url = URL(string: "\(baseURL)/users/bulk-delete") else {
            completion(.failure(.invalidURL))
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "DELETE"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(AuthManager.shared.token)", forHTTPHeaderField: "Authorization")
        
        let bulkDeleteRequest = BulkDeleteRequest(userIDs: userIDs)
        
        do {
            let jsonData = try JSONEncoder().encode(bulkDeleteRequest)
            request.httpBody = jsonData
        } catch {
            completion(.failure(.encodingError(error)))
            return
        }
        
        performDataTask(request: request, completion: completion)
    }
}

Advanced Networking

Custom URLSession Configuration

class AdvancedAPIManager {
    private let session: URLSession
    
    init() {
        let configuration = URLSessionConfiguration.default
        
        // Timeout settings
        configuration.timeoutIntervalForRequest = 30
        configuration.timeoutIntervalForResource = 60
        
        // Cache settings
        configuration.requestCachePolicy = .returnCacheDataElseLoad
        configuration.urlCache = URLCache(
            memoryCapacity: 50 * 1024 * 1024,    // 50 MB memory cache
            diskCapacity: 100 * 1024 * 1024,      // 100 MB disk cache
            diskPath: "api_cache"
        )
        
        // Connection settings
        configuration.allowsCellularAccess = true
        configuration.waitsForConnectivity = true
        configuration.httpMaximumConnectionsPerHost = 4
        
        // Protocol settings
        configuration.protocolClasses = [CustomProtocol.self]
        
        // HTTP settings
        configuration.httpCookieAcceptPolicy = .always
        configuration.httpShouldSetCookies = true
        configuration.httpShouldUsePipelining = true
        
        // Background downloads
        configuration.isDiscretionary = false
        configuration.shouldUseExtendedBackgroundIdleMode = true
        
        self.session = URLSession(
            configuration: configuration,
            delegate: self,
            delegateQueue: nil
        )
    }
}

Request Interceptors and Middleware

protocol RequestInterceptor {
    func intercept(request: inout URLRequest)
}

class AuthenticationInterceptor: RequestInterceptor {
    func intercept(request: inout URLRequest) {
        if let token = AuthManager.shared.currentToken {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
    }
}

class LoggingInterceptor: RequestInterceptor {
    func intercept(request: inout URLRequest) {
        print("🌐 \(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "")")
        
        if let headers = request.allHTTPHeaderFields {
            print("πŸ“‹ Headers: \(headers)")
        }
        
        if let body = request.httpBody {
            print("πŸ“¦ Body: \(String(data: body, encoding: .utf8) ?? "Binary data")")
        }
    }
}

class NetworkManager {
    private let interceptors: [RequestInterceptor]
    
    init(interceptors: [RequestInterceptor] = []) {
        self.interceptors = [
            AuthenticationInterceptor(),
            LoggingInterceptor()
        ] + interceptors
    }
    
    private func applyInterceptors(to request: inout URLRequest) {
        interceptors.forEach { $0.intercept(request: &request) }
    }
}

Response Processing Pipeline

protocol ResponseProcessor {
    func process<T: Codable>(data: Data, response: URLResponse) throws -> T
}

class DefaultResponseProcessor: ResponseProcessor {
    func process<T: Codable>(data: Data, response: URLResponse) throws -> T {
        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }
        
        // Check content type
        if let contentType = httpResponse.allHeaderFields["Content-Type"] as? String {
            guard contentType.contains("application/json") else {
                throw APIError.invalidContentType
            }
        }
        
        // Validate status code
        guard 200...299 ~= httpResponse.statusCode else {
            if let errorData = try? JSONDecoder().decode(APIErrorResponse.self, from: data) {
                throw APIError.apiError(errorData)
            }
            throw APIError.serverError(httpResponse.statusCode)
        }
        
        // Decode response
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        
        do {
            return try decoder.decode(T.self, from: data)
        } catch {
            throw APIError.decodingError(error)
        }
    }
}

Background Downloads

class BackgroundDownloadManager: NSObject {
    static let shared = BackgroundDownloadManager()
    
    private var backgroundSession: URLSession!
    private var activeDownloads: [URL: Download] = [:]
    
    override init() {
        super.init()
        
        let config = URLSessionConfiguration.background(withIdentifier: "com.app.background-downloads")
        config.isDiscretionary = false
        config.sessionSendsLaunchEvents = true
        
        backgroundSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }
    
    func startDownload(url: URL) {
        let download = Download(url: url)
        download.task = backgroundSession.downloadTask(with: url)
        download.task?.resume()
        
        activeDownloads[url] = download
    }
    
    func pauseDownload(url: URL) {
        guard let download = activeDownloads[url] else { return }
        
        download.task?.cancel { resumeDataOrNil in
            guard let resumeData = resumeDataOrNil else { return }
            download.resumeData = resumeData
        }
    }
    
    func resumeDownload(url: URL) {
        guard let download = activeDownloads[url] else { return }
        
        if let resumeData = download.resumeData {
            download.task = backgroundSession.downloadTask(withResumeData: resumeData)
        } else {
            download.task = backgroundSession.downloadTask(with: url)
        }
        
        download.task?.resume()
    }
}

WebSocket Implementation

@available(iOS 13.0, *)
class WebSocketManager: NSObject {
    private var webSocketTask: URLSessionWebSocketTask?
    private let session: URLSession
    
    override init() {
        self.session = URLSession(configuration: .default)
        super.init()
    }
    
    func connect(to url: URL) {
        webSocketTask = session.webSocketTask(with: url)
        webSocketTask?.resume()
        receiveMessage()
    }
    
    func disconnect() {
        webSocketTask?.cancel(with: .goingAway, reason: "User initiated disconnect".data(using: .utf8))
    }
    
    func sendMessage(_ message: String) {
        let message = URLSessionWebSocketTask.Message.string(message)
        webSocketTask?.send(message) { error in
            if let error = error {
                print("WebSocket send error: \(error)")
            }
        }
    }
    
    func sendData(_ data: Data) {
        let message = URLSessionWebSocketTask.Message.data(data)
        webSocketTask?.send(message) { error in
            if let error = error {
                print("WebSocket send error: \(error)")
            }
        }
    }
    
    private func receiveMessage() {
        webSocketTask?.receive { [weak self] result in
            switch result {
            case .success(let message):
                self?.handleMessage(message)
                self?.receiveMessage() // Continue receiving
            case .failure(let error):
                print("WebSocket receive error: \(error)")
            }
        }
    }
    
    private func handleMessage(_ message: URLSessionWebSocketTask.Message) {
        switch message {
        case .string(let text):
            print("Received text: \(text)")
        case .data(let data):
            print("Received data: \(data.count) bytes")
        @unknown default:
            break
        }
    }
}

Error Handling

Robust error handling is crucial for a good user experience and debugging. Here's a comprehensive error handling system:

Custom Error Types

enum APIError: Error, LocalizedError {
    case invalidURL
    case noData
    case invalidResponse
    case networkError(Error)
    case serverError(Int)
    case decodingError(Error)
    case encodingError(Error)
    case authenticationRequired
    case forbidden
    case resourceNotFound
    case rateLimitExceeded
    case invalidContentType
    case apiError(APIErrorResponse)

    // MARK: - LocalizedError
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL"
        case .noData:
            return "No data received"
        case .invalidResponse:
            return "Invalid response"
        case .networkError(let error):
            return "Network error: \(error.localizedDescription)"
        case .serverError(let code):
            return "Server error with status code: \(code)"
        case .decodingError(let error):
            return "Decoding error: \(error.localizedDescription)"
        case .encodingError(let error):
            return "Encoding error: \(error.localizedDescription)"
        case .authenticationRequired:
            return "Authentication required"
        case .forbidden:
            return "Access forbidden"
        case .resourceNotFound:
            return "Resource not found"
        case .rateLimitExceeded:
            return "Rate limit exceeded"
        case .invalidContentType:
            return "Invalid content type"
        case .apiError(let apiError):
            return apiError.message
        }
    }

    // MARK: - Retry logic
    var isRetryable: Bool {
        switch self {
        case .networkError:
            return true // e.g. connectivity issues
        case .serverError(let code):
            // Retry for 5xx errors
            return (500...599).contains(code)
        case .rateLimitExceeded:
            return true // can retry after backoff
        default:
            return false
        }
    }
}

Comments

Popular posts from this blog

Complete iOS Developer Guide - Swift 5 by @hiren_syl |  You Must Have To Know 😎 

Debugging

Swift Fundamentals You Can’t Miss! A List by @hiren_syl