When processing a dynamic list (e.g., fetching 50 user profiles or downloading an array of images), unconstrained concurrency can cause high memory usage or thread pool exhaustion. You need to limit execution to a maximum number of concurrent operations at any given time (e.g., max 3 at once).
Press enter or click to view image in full size
Get Mok5ha G’s stories in your inbox
Join Medium for free to get updates from this writer.
Approach 1: Grand Central Dispatch (DispatchQueue + DispatchSemaphore)
import Foundation
final class BatchDownloaderGCD { private let maxConcurrentTasks: Int private let queue = DispatchQueue(label: "com.app.batchdownloader", attributes: .concurrent)
init(maxConcurrentTasks: Int = 3) { self.maxConcurrentTasks = maxConcurrentTasks }
func downloadAll(urls: [URL], completion: @escaping ([Data]) -> Void) { let semaphore = DispatchSemaphore(value: maxConcurrentTasks) let group = DispatchGroup()
var results: [Data] = [] let lock = NSLock() // Thread-safe array mutations
for url in urls { group.enter() queue.async { semaphore.wait() // Block if max concurrent limit is reached
URLSession.shared.dataTask(with: url) { data, _, _ in defer { semaphore.signal() // Free up slot for next task group.leave() }
if let data = data { lock.lock() results.append(data) lock.unlock() } }.resume() } }
Approach 2: Swift Concurrency (withThrowingTaskGroup)
import Foundation
final class BatchDownloaderAsync { private let maxConcurrentTasks: Int
init(maxConcurrentTasks: Int = 3) { self.maxConcurrentTasks = maxConcurrentTasks }
func downloadAll(urls: [URL]) async throws -> [Data] { try await withThrowingTaskGroup(of: Data?.self) { group in var results: [Data] = [] var urlIterator = urls.makeIterator()
// Seed initial batch up to max limit for _ in 0..<maxConcurrentTasks { if let url = urlIterator.next() { group.addTask { let (data, _) = try await URLSession.shared.data(from: url) return data } } }
// As each task finishes, collect result and spin up next task while let data = try await group.next() { if let data = data { results.append(data) }
if let nextURL = urlIterator.next() { group.addTask { let (data, _) = try await URLSession.shared.data(from: nextURL) return data } } }
return results } } }
SwiftUI Usage Example
import SwiftUI
struct BatchImageLoaderView: View { private let downloader = BatchDownloaderAsync(maxConcurrentTasks: 3)
var body: some View { Button("Download Batch") { Task { let sampleURLs = (1...20).compactMap { URL(string: "https://picsum.photos/id/\($0)/200/200") }
if let images = try? await downloader.downloadAll(urls: sampleURLs) { print("Successfully downloaded \(images.count) images batch-wise") } } } } }