arrow_back Back to Blog

🌐 iOS Networking with Async/Await

Modern networking patterns using async/await in iOS

schedule 6 min read calendar_today December 30, 2023

Modern Networking

Swift's async/await makes networking code more readable and maintainable compared to traditional completion handlers.

1. Basic Async/Await Networking

// Traditional completion handler approach
func fetchUser(completion: @escaping (Result) -> Void) {
    URLSession.shared.dataTask(with: userURL) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let data = data else {
            completion(.failure(NetworkError.noData))
            return
        }
        
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

// Modern async/await approach
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: userURL)
    let user = try JSONDecoder().decode(User.self, from: data)
    return user
}

2. Network Service Layer

protocol NetworkServiceProtocol {
    func request(_ endpoint: Endpoint) async throws -> T
}

class NetworkService: NetworkServiceProtocol {
    private let session: URLSession
    
    init(session: URLSession = .shared) {
        self.session = session
    }
    
    func request(_ endpoint: Endpoint) async throws -> T {
        let request = try endpoint.urlRequest()
        let (data, response) = try await session.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              200...299 ~= httpResponse.statusCode else {
            throw NetworkError.invalidResponse
        }
        
        return try JSONDecoder().decode(T.self, from: data)
    }
}

// Usage
class UserService {
    private let networkService: NetworkServiceProtocol
    
    init(networkService: NetworkServiceProtocol = NetworkService()) {
        self.networkService = networkService
    }
    
    func fetchUser() async throws -> User {
        return try await networkService.request(.user)
    }
    
    func createUser(_ user: CreateUserRequest) async throws -> User {
        return try await networkService.request(.createUser(user))
    }
}

3. Error Handling

enum NetworkError: Error, LocalizedError {
    case invalidURL
    case noData
    case invalidResponse
    case decodingError(Error)
    
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL"
        case .noData:
            return "No data received"
        case .invalidResponse:
            return "Invalid response"
        case .decodingError(let error):
            return "Decoding error: \(error.localizedDescription)"
        }
    }
}

// Usage with error handling
func loadUser() async {
    do {
        let user = try await userService.fetchUser()
        await MainActor.run {
            self.user = user
        }
    } catch {
        await MainActor.run {
            self.errorMessage = error.localizedDescription
        }
    }
}

4. Concurrent Requests

// Sequential execution (slow)
func loadDataSequentially() async throws {
    let user = try await fetchUser()
    let posts = try await fetchPosts()
    let comments = try await fetchComments()
    
    // Process data...
}

// Concurrent execution (fast)
func loadDataConcurrently() async throws {
    async let user = fetchUser()
    async let posts = fetchPosts()
    async let comments = fetchComments()
    
    // All three requests happen simultaneously
    let (userData, postsData, commentsData) = try await (user, posts, comments)
    
    // Process data...
}

// Using TaskGroup for dynamic concurrency
func downloadImages(urls: [URL]) async -> [UIImage] {
    await withTaskGroup(of: UIImage?.self) { group in
        var images: [UIImage] = []
        
        for url in urls {
            group.addTask {
                return try? await downloadImage(from: url)
            }
        }
        
        for await image in group {
            if let image = image {
                images.append(image)
            }
        }
        
        return images
    }
}

5. Best Practices

✅ Do's

  • Use async/await for new networking code
  • Handle errors appropriately
  • Use concurrent requests when possible
  • Test your networking code
  • Use proper error types

❌ Don'ts

  • Don't mix async/await with completion handlers unnecessarily
  • Don't ignore errors
  • Don't perform network requests on the main thread
  • Don't forget to handle cancellation