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