How to Call Multiple APIs Concurrently in Swift

Master Concurrent API Calls: Swift Techniques for Modern iOS Apps

Vikram Kumar
4 min readJan 6, 2025

Introduction

In modern iOS applications, there are many scenarios where you need to call multiple APIs concurrently to optimize network operations and enhance user experience. Swift provides several ways to achieve this, ranging from traditional approaches to more modern, declarative methods. This article covers various methods to call multiple APIs concurrently in Swift, detailing their implementation and pros/cons.

Photo by Christopher Gower on Unsplash

1. Using DispatchGroup

DispatchGroup is a core part of Grand Central Dispatch (GCD) and is ideal for managing multiple asynchronous tasks.

Implementation:

func fetchAPIData() {
let group = DispatchGroup()

let urls = [
URL(string: "https://jsonplaceholder.typicode.com/todos/1")!,
URL(string: "https://jsonplaceholder.typicode.com/todos/2")!,
URL(string: "https://jsonplaceholder.typicode.com/todos/3")!
]

var results: [Data?] = Array(repeating: nil, count: urls.count)

for (index, url) in urls.enumerated() {
group.enter()
URLSession.shared.dataTask(with: url) { data, _, _ in
results[index] = data
group.leave()
}.resume()
}

group.notify(queue: .main) {
print("All API calls are completed.")
for result in results {
if let data = result {
print("Response Data: \(String(data: data, encoding: .utf8) ?? "")")
}
}
}
}

Pros:

  • Simple and effective for most use cases.
  • Allows monitoring the completion of all tasks.

Cons:

  • Managing large-scale operations can be tricky.
  • Error handling and individual task cancellation are cumbersome.

2. Using OperationQueue

OperationQueue is part of the Foundation framework and provides more control over task dependencies and priorities.

Implementation:

func fetchAPIDataWithOperationQueue() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3 // Limit concurrency if needed

let urls = [
URL(string: "https://jsonplaceholder.typicode.com/todos/4")!,
URL(string: "https://jsonplaceholder.typicode.com/todos/5")!,
URL(string: "https://jsonplaceholder.typicode.com/todos/6")!
]

let completionOperation = BlockOperation {
print("All API calls are completed.")
}

for url in urls {
let operation = BlockOperation {
let semaphore = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data = data {
print("Response Data: \(String(data: data, encoding: .utf8) ?? "")")
}
semaphore.signal()
}.resume()
semaphore.wait()
}
completionOperation.addDependency(operation)
queue.addOperation(operation)
}

queue.addOperation(completionOperation)
}

Pros:

  • Offers more control over task dependencies and execution order.
  • Easy to limit concurrency.

Cons:

  • Verbose for simple tasks.
  • Requires careful handling to avoid deadlocks.

3. Using Combine

Combine provides a declarative Swift API for handling asynchronous events, making it a powerful choice for concurrent API calls.

Implementation:

import Foundation
import Combine

var cancellables = Set<AnyCancellable>()

func fetchAPIDataWithCombine() {
let urls = [
URL(string: "https://jsonplaceholder.typicode.com/todos/10")!,
URL(string: "https://jsonplaceholder.typicode.com/todos/11")!,
URL(string: "https://jsonplaceholder.typicode.com/todos/12")!
]

let publishers = urls.map { url in
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data) // Corrected key path
.catch { _ in Just(Data()) } // Handle errors gracefully
.eraseToAnyPublisher() // Resolve type ambiguity
}

Publishers.MergeMany(publishers)
.collect() // Gather all responses into an array
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .finished:
print("All API calls are completed.")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
} receiveValue: { dataArray in
dataArray.forEach { data in
print("Response Data: \(String(data: data, encoding: .utf8) ?? "")")
}
}
.store(in: &cancellables)
}

Pros:

  • Declarative and modern.
  • Error handling is built-in and clean.
  • Works seamlessly with SwiftUI.

Cons:

  • Requires knowledge of Combine.
  • Limited to iOS 13 and above.

4. Using Async/Await

The async/await pattern simplifies asynchronous code, introduced in Swift 5.5.

Implementation:

func fetchAPIDataWithAsyncAwait() async {
let urls = [
URL(string: "https://jsonplaceholder.typicode.com/todos/7")!,
URL(string: "https://jsonplaceholder.typicode.com/todos/8")!,
URL(string: "https://jsonplaceholder.typicode.com/todos/9")!
]

await withTaskGroup(of: (Int, Data?).self) { group in
for (index, url) in urls.enumerated() {
group.addTask {
do {
let (data, _) = try await URLSession.shared.data(from: url)
return (index, data)
} catch {
return (index, nil)
}
}
}

var results = Array(repeating: Data?.none, count: urls.count)

for await (index, data) in group {
results[index] = data
}

print("All API calls are completed.")
for result in results {
if let data = result {
print("Response Data: \(String(data: data, encoding: .utf8) ?? "")")
}
}
}
}

Pros:

  • Clean and readable syntax.
  • Integrated into Swift’s concurrency model.
  • Provides structured error handling.

Cons:

  • Requires iOS 15 or macOS 12 and above.

Comparison Summary

DispatchGroup:

  • Pros: Simple and effective for straightforward tasks.
  • Cons: Tricky for large-scale operations; error handling is hard.
  • Minimum iOS Version: iOS 7.

OperationQueue:

  • Pros: Provides control over dependencies and priorities.
  • Cons: Verbose for simple tasks; risk of deadlocks.
  • Minimum iOS Version: iOS 7.

Combine:

  • Pros: Declarative, modern, and integrates with SwiftUI.
  • Cons: Requires knowledge of Combine; limited to iOS 13+.
  • Minimum iOS Version: iOS 13.

Async/Await:

  • Pros: Clean, readable, and structured error handling.
  • Cons: Requires iOS 15+; not backward compatible.
  • Minimum iOS Version: iOS 15.

Conclusion

Calling multiple APIs concurrently is an essential skill for modern iOS development. Depending on your app’s requirements, you can choose any of the methods discussed:

  • DispatchGroup: For straightforward use cases.
  • OperationQueue: When more control over concurrency is required.
  • Combine: For declarative and reactive programming.
  • Async/Await: For the most modern and clean approach.

Choose the approach that best aligns with your app’s architecture and deployment target. Happy coding!

Thank you for reading until the end. Before you go:

--

--

Vikram Kumar
Vikram Kumar

Written by Vikram Kumar

I am Vikram, a Senior iOS Developer at Matellio Inc. focused on writing clean and efficient code. Complex problem-solver with an analytical and driven mindset.

Responses (1)