Objective-C
Asynchronous Programming
Looping
Networking
iOS Development

Looping and asynchronous connections in objective-c

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When making multiple network requests in a loop in Objective-C, you need to handle the asynchronous nature of network operations carefully. A naive for loop fires all requests simultaneously, and the loop finishes before any response arrives. To process responses in order or wait for all to complete, use dispatch groups (dispatch_group_t), semaphores, or NSOperationQueue with dependencies. The key challenge is coordinating loop iteration with asynchronous callbacks so that data is collected correctly and the UI updates at the right time.

The Problem: Async Calls in a Loop

objectivec
1// WRONG — loop finishes before any response arrives
2NSArray *urls = @[@"https://api.example.com/1",
3                  @"https://api.example.com/2",
4                  @"https://api.example.com/3"];
5NSMutableArray *results = [NSMutableArray array];
6
7for (NSString *urlString in urls) {
8    NSURL *url = [NSURL URLWithString:urlString];
9    NSURLSessionDataTask *task =
10        [[NSURLSession sharedSession] dataTaskWithURL:url
11            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
12                // This runs LATER, after the loop has finished
13                [results addObject:data];
14            }];
15    [task resume];
16}
17
18// results is EMPTY here — all tasks are still in-flight
19NSLog(@"Results count: %lu", (unsigned long)results.count); // 0

Solution 1: Dispatch Groups

dispatch_group_t tracks a set of async tasks and notifies when all complete:

objectivec
1NSArray *urls = @[@"https://api.example.com/1",
2                  @"https://api.example.com/2",
3                  @"https://api.example.com/3"];
4NSMutableArray *results = [NSMutableArray array];
5dispatch_group_t group = dispatch_group_create();
6
7for (NSString *urlString in urls) {
8    dispatch_group_enter(group);  // Increment counter
9
10    NSURL *url = [NSURL URLWithString:urlString];
11    NSURLSessionDataTask *task =
12        [[NSURLSession sharedSession] dataTaskWithURL:url
13            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
14                if (data) {
15                    @synchronized (results) {
16                        [results addObject:data];
17                    }
18                }
19                dispatch_group_leave(group);  // Decrement counter
20            }];
21    [task resume];
22}
23
24// Called when ALL tasks complete
25dispatch_group_notify(group, dispatch_get_main_queue(), ^{
26    NSLog(@"All done! Results: %lu", (unsigned long)results.count);
27    // Update UI here
28});

With Timeout

objectivec
1dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC);
2long result = dispatch_group_wait(group, timeout);
3
4if (result == 0) {
5    NSLog(@"All tasks completed within 30 seconds");
6} else {
7    NSLog(@"Timeout — some tasks did not finish");
8}

Solution 2: Semaphores for Sequential Execution

To execute requests one at a time (waiting for each to finish before starting the next):

objectivec
1dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
2NSMutableArray *results = [NSMutableArray array];
3NSArray *urls = @[@"https://api.example.com/1",
4                  @"https://api.example.com/2",
5                  @"https://api.example.com/3"];
6
7dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
8    for (NSString *urlString in urls) {
9        NSURL *url = [NSURL URLWithString:urlString];
10        NSURLSessionDataTask *task =
11            [[NSURLSession sharedSession] dataTaskWithURL:url
12                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
13                    if (data) {
14                        [results addObject:data];
15                    }
16                    dispatch_semaphore_signal(semaphore);  // Unblock
17                }];
18        [task resume];
19        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);  // Block until done
20    }
21
22    dispatch_async(dispatch_get_main_queue(), ^{
23        NSLog(@"All sequential requests done: %lu", (unsigned long)results.count);
24    });
25});

Solution 3: NSOperationQueue

NSOperationQueue provides higher-level control with dependencies and concurrency limits:

objectivec
1NSOperationQueue *queue = [[NSOperationQueue alloc] init];
2queue.maxConcurrentOperationCount = 3;  // Limit concurrent requests
3
4NSArray *urls = @[@"https://api.example.com/1",
5                  @"https://api.example.com/2",
6                  @"https://api.example.com/3"];
7
8NSBlockOperation *completionOp = [NSBlockOperation blockOperationWithBlock:^{
9    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
10        NSLog(@"All operations finished — update UI");
11    }];
12}];
13
14for (NSString *urlString in urls) {
15    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
16        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
17        NSURL *url = [NSURL URLWithString:urlString];
18
19        NSURLSessionDataTask *task =
20            [[NSURLSession sharedSession] dataTaskWithURL:url
21                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
22                    // Process data
23                    dispatch_semaphore_signal(sem);
24                }];
25        [task resume];
26        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
27    }];
28
29    [completionOp addDependency:op];  // Completion waits for all
30    [queue addOperation:op];
31}
32
33[queue addOperation:completionOp];

Solution 4: Recursive Chaining

Process one request at a time using recursive method calls:

objectivec
1- (void)fetchURLs:(NSArray *)urls
2          atIndex:(NSUInteger)index
3          results:(NSMutableArray *)results
4       completion:(void (^)(NSArray *))completion {
5
6    if (index >= urls.count) {
7        completion([results copy]);
8        return;
9    }
10
11    NSURL *url = [NSURL URLWithString:urls[index]];
12    NSURLSessionDataTask *task =
13        [[NSURLSession sharedSession] dataTaskWithURL:url
14            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
15                if (data) {
16                    [results addObject:data];
17                }
18                // Recursively fetch the next URL
19                [self fetchURLs:urls
20                        atIndex:index + 1
21                        results:results
22                     completion:completion];
23            }];
24    [task resume];
25}
26
27// Usage
28NSArray *urls = @[@"https://api.example.com/1",
29                  @"https://api.example.com/2",
30                  @"https://api.example.com/3"];
31
32[self fetchURLs:urls atIndex:0 results:[NSMutableArray array]
33     completion:^(NSArray *results) {
34    dispatch_async(dispatch_get_main_queue(), ^{
35        NSLog(@"Fetched %lu results in order", (unsigned long)results.count);
36    });
37}];

Approach Comparison

ApproachConcurrent?Ordered?Complexity
Dispatch groupYesNoLow
SemaphoreNo (sequential)YesLow
NSOperationQueueConfigurableWith dependenciesMedium
Recursive chainingNo (sequential)YesMedium

Common Pitfalls

  • Forgetting dispatch_group_leave in error paths: Every dispatch_group_enter must be paired with a dispatch_group_leave, even when the request fails. A missing leave call means dispatch_group_notify never fires, and your app silently hangs.
  • Calling dispatch_semaphore_wait on the main thread: Blocking the main thread with a semaphore freezes the UI and can cause watchdog termination on iOS. Always dispatch semaphore-based loops to a background queue.
  • Mutating a shared array without synchronization: Completion handlers from multiple concurrent requests run on different threads. Mutating an NSMutableArray without @synchronized or a serial queue causes race conditions and crashes.
  • Not dispatching UI updates to the main queue: Completion handlers from NSURLSession run on a background queue by default. Calling UIKit methods directly from these handlers causes undefined behavior. Always use dispatch_async(dispatch_get_main_queue(), ...).
  • Assuming loop order equals response order: With concurrent requests, responses arrive in arbitrary order. If order matters, either use sequential execution (semaphore/recursive) or store results by index instead of appending.

Summary

  • Async network calls in a loop complete after the loop finishes — you cannot use the results immediately
  • Use dispatch_group_t with enter/leave for concurrent requests with a completion callback
  • Use dispatch_semaphore_t on a background queue for sequential one-at-a-time execution
  • Use NSOperationQueue for configurable concurrency limits and operation dependencies
  • Always synchronize access to shared mutable collections and dispatch UI updates to the main queue

Course illustration
Course illustration

All Rights Reserved.