import Foundation
enum APIError: Error, LocalizedError {
case invalidURL
case invalidResponse
case statusCode(Int)
case decodingError(Error)
case networkError(Error)
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL"
case .invalidResponse:
return "Invalid response from server"
case .statusCode(let code):
return "Server returned status code \(code)"
case .decodingError(let error):
return "Failed to decode response: \(error.localizedDescription)"
case .networkError(let error):
return "Network error: \(error.localizedDescription)"
}
}
}
class APIService {
static let shared = APIService()
private let baseURL = "https://api.example.com"
private let session: URLSession
private init() {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
self.session = URLSession(configuration: configuration)
}
func fetchPosts() async throws -> [Post] {
guard let url = URL(string: "\(baseURL)/posts") else {
throw APIError.invalidURL
}
do {
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw APIError.statusCode(httpResponse.statusCode)
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let postsResponse = try decoder.decode(PostsResponse.self, from: data)
return postsResponse.posts
} catch {
throw APIError.decodingError(error)
}
} catch let error as APIError {
throw error
} catch {
throw APIError.networkError(error)
}
}
func createPost(title: String, body: String) async throws -> Post {
guard let url = URL(string: "\(baseURL)/posts") else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(AuthManager.shared.token ?? "")", forHTTPHeaderField: "Authorization")
let postData = CreatePostRequest(title: title, body: body)
request.httpBody = try JSONEncoder().encode(postData)
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.invalidResponse
}
return try JSONDecoder().decode(Post.self, from: data)
}
}
struct CreatePostRequest: Encodable {
let title: String
let body: String
}
Modern Swift networking uses async/await for cleaner asynchronous code compared to completion handlers. URLSession's async methods like data(from:) make network calls straightforward. I wrap API calls in a service layer with typed responses using Codable. Error handling uses try/catch blocks instead of nested closures. For request configuration, I create URLRequest objects with custom headers, HTTP methods, and body data. Response validation checks status codes before decoding. Async/await integrates seamlessly with SwiftUI—mark view methods with async and call with Task. This modern approach eliminates callback hell and makes async code read linearly like synchronous code.