🌐 iOS Networking with Async/Await
Modern networking patterns using async/await in iOS
6 min read
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