Modern Networking Architecture

Building robust networking layers using Swift's modern async/await pattern and protocol-oriented programming. These patterns are used in production iOS applications.



1. Protocol-Oriented Network Layer

// Network service protocol
protocol NetworkServiceProtocol {
    func request(_ endpoint: Endpoint) async throws -> T
    func upload(_ data: Data, to endpoint: Endpoint) async throws -> Data
    func download(from url: URL) async throws -> Data
}

// Endpoint definition
struct Endpoint {
    let path: String
    let method: HTTPMethod
    let headers: [String: String]?
    let body: Data?
    
    enum HTTPMethod: String {
        case GET, POST, PUT, DELETE, PATCH
    }
}

// Network service implementation
actor NetworkService: NetworkServiceProtocol {
    private let session: URLSession
    private let baseURL: URL
    
    init(baseURL: URL, session: URLSession = .shared) {
        self.baseURL = baseURL
        self.session = session
    }
    
    func request(_ endpoint: Endpoint) async throws -> T {
        let url = baseURL.appendingPathComponent(endpoint.path)
        var request = URLRequest(url: url)
        request.httpMethod = endpoint.method.rawValue
        request.httpBody = endpoint.body
        
        // Add headers
        endpoint.headers?.forEach { key, value in
            request.setValue(value, forHTTPHeaderField: key)
        }
        
        let (data, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }
        
        guard 200...299 ~= httpResponse.statusCode else {
            throw NetworkError.httpError(httpResponse.statusCode)
        }
        
        return try JSONDecoder().decode(T.self, from: data)
    }
}





2. Error Handling

enum NetworkError: Error, LocalizedError {
    case invalidURL
    case invalidResponse
    case httpError(Int)
    case decodingError
    case noData
    case unauthorized
    case serverError
    
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL"
        case .invalidResponse:
            return "Invalid response"
        case .httpError(let code):
            return "HTTP Error: \(code)"
        case .decodingError:
            return "Failed to decode response"
        case .noData:
            return "No data received"
        case .unauthorized:
            return "Unauthorized access"
        case .serverError:
            return "Server error"
        }
    }
}





3. Practical Usage Examples

// API service for user management
class UserService {
    private let networkService: NetworkServiceProtocol
    
    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }
    
    func fetchUser(id: String) async throws -> User {
        let endpoint = Endpoint(
            path: "users/\(id)",
            method: .GET,
            headers: ["Content-Type": "application/json"],
            body: nil
        )
        
        return try await networkService.request(endpoint)
    }
    
    func createUser(_ user: CreateUserRequest) async throws -> User {
        let body = try JSONEncoder().encode(user)
        let endpoint = Endpoint(
            path: "users",
            method: .POST,
            headers: ["Content-Type": "application/json"],
            body: body
        )
        
        return try await networkService.request(endpoint)
    }
}

// Usage in ViewModel
@MainActor
class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let userService: UserService
    
    init(userService: UserService) {
        self.userService = userService
    }
    
    func loadUser(id: String) async {
        isLoading = true
        errorMessage = nil
        
        do {
            user = try await userService.fetchUser(id: id)
        } catch {
            errorMessage = error.localizedDescription
        }
        
        isLoading = false
    }
}





4. Authentication & Token Management

actor AuthenticatedNetworkService: NetworkServiceProtocol {
    private let baseService: NetworkServiceProtocol
    private var authToken: String?
    
    init(baseService: NetworkServiceProtocol) {
        self.baseService = baseService
    }
    
    func setAuthToken(_ token: String) {
        self.authToken = token
    }
    
    func request(_ endpoint: Endpoint) async throws -> T {
        var authenticatedEndpoint = endpoint
        
        // Add auth header if token exists
        if let token = authToken {
            var headers = endpoint.headers ?? [:]
            headers["Authorization"] = "Bearer \(token)"
            authenticatedEndpoint = Endpoint(
                path: endpoint.path,
                method: endpoint.method,
                headers: headers,
                body: endpoint.body
            )
        }
        
        do {
            return try await baseService.request(authenticatedEndpoint)
        } catch NetworkError.httpError(401) {
            // Token expired, refresh and retry
            try await refreshToken()
            return try await baseService.request(authenticatedEndpoint)
        }
    }
    
    private func refreshToken() async throws {
        // Implement token refresh logic
    }
}





5. Caching Strategy

actor CachingNetworkService: NetworkServiceProtocol {
    private let baseService: NetworkServiceProtocol
    private var cache: [String: CachedResponse] = [:]
    
    struct CachedResponse {
        let data: Data
        let timestamp: Date
        let maxAge: TimeInterval
        
        var isExpired: Bool {
            Date().timeIntervalSince(timestamp) > maxAge
        }
    }
    
    init(baseService: NetworkServiceProtocol) {
        self.baseService = baseService
    }
    
    func request(_ endpoint: Endpoint) async throws -> T {
        let cacheKey = "\(endpoint.method.rawValue):\(endpoint.path)"
        
        // Check cache for GET requests
        if endpoint.method == .GET,
           let cachedResponse = cache[cacheKey],
           !cachedResponse.isExpired {
            return try JSONDecoder().decode(T.self, from: cachedResponse.data)
        }
        
        // Make network request
        let result: T = try await baseService.request(endpoint)
        
        // Cache GET responses
        if endpoint.method == .GET {
            let data = try JSONEncoder().encode(result)
            cache[cacheKey] = CachedResponse(
                data: data,
                timestamp: Date(),
                maxAge: 300 // 5 minutes
            )
        }
        
        return result
    }
}





6. Testing Network Code

// Mock network service for testing
class MockNetworkService: NetworkServiceProtocol {
    var responses: [String: Result] = [:]
    
    func request(_ endpoint: Endpoint) async throws -> T {
        let key = "\(endpoint.method.rawValue):\(endpoint.path)"
        
        guard let response = responses[key] else {
            throw NetworkError.noData
        }
        
        let data = try response.get()
        return try JSONDecoder().decode(T.self, from: data)
    }
}

// Unit test example
class UserServiceTests: XCTestCase {
    func testFetchUser() async throws {
        // Given
        let mockService = MockNetworkService()
        let userService = UserService(networkService: mockService)
        
        let expectedUser = User(id: "1", name: "John Doe", email: "john@example.com")
        let userData = try JSONEncoder().encode(expectedUser)
        mockService.responses["GET:users/1"] = .success(userData)
        
        // When
        let user = try await userService.fetchUser(id: "1")
        
        // Then
        XCTAssertEqual(user.name, "John Doe")
        XCTAssertEqual(user.email, "john@example.com")
    }
}





Best Practices

  • Use actors for network services to ensure thread safety
  • Implement proper error handling with custom error types
  • Add authentication layers for secure API access
  • Cache responses when appropriate to improve performance
  • Use protocol-oriented design for testability
  • Handle network timeouts and retries gracefully