Skip to content
All essays
iOSMarch 27, 202515 min

Network Programming in Swift: URLSession and Beyond

Master network programming in Swift. URLSession, async/await, error handling, and best practices.

Ü
Ümit Uz
Mobile & Full Stack Developer

Network programming is essential for modern iOS apps. Learn to make HTTP requests, handle errors, and build robust network layers.

URLSession Basics

Simple GET Request

swift
import Foundation

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

class NetworkManager {
    func fetchUsers() async throws -> [User] {
        let url = URL(string: "https://api.example.com/users")!

        let (data, _) = try await URLSession.shared.data(from: url)

        return try JSONDecoder().decode([User].self, from: data)
    }
}

POST Request

swift
struct Post: Codable {
    let title: String
    let body: String
    let userId: Int
}

class NetworkManager {
    func createPost(title: String, body: String) async throws -> Post {
        let url = URL(string: "https://api.example.com/posts")!

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let post = Post(title: title, body: body, userId: 1)
        request.httpBody = try JSONEncoder().encode(post)

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 201 else {
            throw NetworkError.invalidResponse
        }

        return try JSONDecoder().decode(Post.self, from: data)
    }
}

enum NetworkError: Error {
    case invalidURL
    case invalidResponse
    case decodingFailed
}

Error Handling

Comprehensive Error Handling

swift
enum NetworkError: Error, LocalizedError {
    case invalidURL
    case requestFailed(Error)
    case invalidResponse
    case httpError(statusCode: Int)
    case decodingFailed(Error)
    case encodingFailed(Error)

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL provided"
        case .requestFailed(let error):
            return "Request failed: \(error.localizedDescription)"
        case .invalidResponse:
            return "Invalid response from server"
        case .httpError(let statusCode):
            return "HTTP error with status code: \(statusCode)"
        case .decodingFailed(let error):
            return "Decoding failed: \(error.localizedDescription)"
        case .encodingFailed(let error):
            return "Encoding failed: \(error.localizedDescription)"
        }
    }
}

class NetworkManager {
    func fetchData<T: Decodable>(from url: URL) async throws -> T {
        do {
            let (data, response) = try await URLSession.shared.data(from: url)

            guard let httpResponse = response as? HTTPURLResponse else {
                throw NetworkError.invalidResponse
            }

            guard (200...299).contains(httpResponse.statusCode) else {
                throw NetworkError.httpError(statusCode: httpResponse.statusCode)
            }

            do {
                return try JSONDecoder().decode(T.self, from: data)
            } catch {
                throw NetworkError.decodingFailed(error)
            }

        } catch let error as NetworkError {
            throw error
        } catch {
            throw NetworkError.requestFailed(error)
        }
    }
}

Request Configuration

Custom URLRequest

swift
struct URLRequestBuilder {
    let baseURL: URL
    var headers: [String: String] = [:]
    var timeout: TimeInterval = 30
    var cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy

    func build(path: String, method: HTTPMethod = .get) -> URLRequest {
        var url = baseURL.appendingPathComponent(path)

        // Add query parameters for GET requests
        if method == .get, !queryParameters.isEmpty {
            var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
            components.queryItems = queryParameters.map { key, value in
                URLQueryItem(name: key, value: value)
            }
            url = components.url!
        }

        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.timeoutInterval = timeout
        request.cachePolicy = cachePolicy

        for (key, value) in headers {
            request.setValue(value, forHTTPHeaderField: key)
        }

        return request
    }
}

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case patch = "PATCH"
    case delete = "DELETE"
}

Authentication

Bearer Token

swift
class AuthenticatedNetworkManager {
    private let tokenManager: TokenManager

    init(tokenManager: TokenManager) {
        self.tokenManager = tokenManager
    }

    func request<T: Decodable>(
        _ url: URL,
        method: HTTPMethod = .get
    ) async throws -> T {
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue

        // Add bearer token
        let token = try await tokenManager.getCurrentToken()
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

        let (data, response) = try await URLSession.shared.data(for: request)

        // Handle token expiration
        if let httpResponse = response as? HTTPURLResponse,
           httpResponse.statusCode == 401 {
            try await tokenManager.refreshToken()
            return try await self.request(url, method: method)
        }

        return try JSONDecoder().decode(T.self, from: data)
    }
}

API Keys

swift
struct APIConfiguration {
    let apiKey: String
    let baseURL: URL

    static let defaultConfig = APIConfiguration(
        apiKey: "your-api-key",
        baseURL: URL(string: "https://api.example.com")!
    )
}

class NetworkManager {
    let configuration: APIConfiguration

    init(configuration: APIConfiguration = .default) {
        self.configuration = configuration
    }

    func createRequest(path: String) -> URLRequest {
        var request = URLRequest(url: configuration.baseURL.appendingPathComponent(path))
        request.setValue(configuration.apiKey, forHTTPHeaderField: "X-API-Key")
        return request
    }
}

Image Loading

Async Image Loading

swift
class ImageLoader: ObservableObject {
    @Published var image: Image?
    @Published var isLoading = false

    private let cache: ImageCache
    private let urlSession: URLSession

    init(cache: ImageCache = .shared, urlSession: URLSession = .shared) {
        self.cache = cache
        self.urlSession = urlSession
    }

    func loadImage(from url: URL) async {
        isLoading = true
        defer { isLoading = false }

        // Check cache first
        if let cached = await cache.getImage(for: url) {
            self.image = cached
            return
        }

        // Download image
        do {
            let (data, _) = try await urlSession.data(from: url)
            if let uiImage = UIImage(data: data) {
                let image = Image(uiImage: uiImage)
                self.image = image
                await cache.setImage(image, for: url)
            }
        } catch {
            print("Failed to load image: \(error)")
        }
    }
}

actor ImageCache {
    static let shared = ImageCache()
    private var storage: [URL: Image] = [:]

    func getImage(for url: URL) -> Image? {
        storage[url]
    }

    func setImage(_ image: Image, for url: URL) {
        storage[url] = image
    }
}

Networking Layer Architecture

Protocol-Based Design

swift
protocol NetworkServiceProtocol {
    func request<T: Decodable>(
        _ endpoint: Endpoint,
        responseType: T.Type
    ) async throws -> T
}

enum Endpoint {
    case users
    case user(id: Int)
    case posts
    case post(id: Int)

    var path: String {
        switch self {
        case .users:
            return "/users"
        case .user(let id):
            return "/users/\(id)"
        case .posts:
            return "/posts"
        case .post(let id):
            return "/posts/\(id)"
        }
    }
}

class NetworkService: NetworkServiceProtocol {
    private let baseURL: URL
    private let session: URLSession

    init(baseURL: URL, session: URLSession = .shared) {
        self.baseURL = baseURL
        self.session = session
    }

    func request<T: Decodable>(
        _ endpoint: Endpoint,
        responseType: T.Type
    ) async throws -> T {
        let url = baseURL.appendingPathComponent(endpoint.path)
        let (data, response) = try await session.data(from: url)

        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw NetworkError.invalidResponse
        }

        return try JSONDecoder().decode(T.self, from: data)
    }
}

// Usage
let service = NetworkService(
    baseURL: URL(string: "https://jsonplaceholder.typicode.com")!
)

let users: [User] = try await service.request(.users, responseType: [User].self)

Best Practices

  1. 1Use async/await: Modern concurrency for network calls
  2. 2Error handling: Comprehensive error handling and recovery
  3. 3Caching: Cache responses to reduce network calls
  4. 4Authentication: Secure token management
  5. 5Request retry: Implement retry logic for failed requests
  6. 6Offline support: Handle offline scenarios gracefully
  7. 7Logging: Log network requests for debugging
  8. 8Testing: Mock network responses for testing

Network programming is crucial for connected iOS apps. Build robust and efficient network layers!

Related essays

Next essay
SwiftUI Navigation Complete Guide