Sitemap

Modern Swift Patterns: Dynamic Task Batching

2 min readOct 24, 2026

--

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()
}
}

group.notify(queue: .main) {
completion(results)
}
}
}

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")
}
}
}
}
}

--

--