Skip to content

Blocks

Hub › iOS › Objective-C › Blocks

Goal

You will read and write Objective-C blocks — C-level closures that capture surrounding state. You will master the famously awkward block syntax (and the typedef that tames it), understand capture-by-value vs __block, and apply the weak/strong dance to break retain cycles. After this page you can write a network method with a completion handler.

Prerequisites

What a block is

A block is an anonymous function — a closure — that captures the variables in scope where it is defined. It is a C language extension (Blocks are to ObjC what closures/lambdas are to Swift): a chunk of code you can pass around, that remembers the variables it was created with.

You use blocks constantly in iOS:

  • Completion handlers[service fetchWithCompletion:^{ ... }].
  • Enumeration[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){ ... }].
  • GCDdispatch_async(queue, ^{ ... }).
  • Animations[UIView animateWithDuration:0.3 animations:^{ ... }].

Block syntax

Block syntax is the hardest part of ObjC for newcomers, because the type declaration reads inside-out. Break it into two pieces: the declaration (a variable that holds a block) and the literal (the block itself).

Declaring a block variable

objc
void (^simpleBlock)(void);

Read this as: "simpleBlock is a block that takes void (no parameters) and returns void." The caret (^) marks it as a block type. The general shape is:

returnType (^blockName)(parameterTypes);

Assigning a block literal

objc
simpleBlock = ^{
    NSLog(@"hello");
};

A block literal starts with ^ and the body is in braces. When the block takes no arguments and returns void, you can omit the parameter list.

Blocks with parameters

objc
void (^greet)(NSString *) = ^(NSString *name) {
    NSLog(@"hi %@", name);
};
greet(@"Mei");    // hi Mei

For a block that returns a value, write the return type before the caret, or let the compiler infer it:

objc
// Explicit return type:
int (^square)(int) = ^int(int x) {
    return x * x;
};

// Inferred return type (the compiler sees `return x * x` → int):
int (^alsoSquare)(int) = ^(int x) {
    return x * x;
};

typedef — tame the syntax

Writing void (^)(NSString *) inline is unreadable. The idiomatic fix is a typedef that names the block type, after which you use it like any other type:

objc
typedef void (^CompletionHandler)(BOOL success, NSError *error);

// Now the type reads cleanly everywhere:
@property (nonatomic, copy) CompletionHandler onComplete;

- (void)fetchWithCompletion:(CompletionHandler)completion;

Always typedef public block types

Any block type that appears in a public header (a property, a method parameter, a return value) should get a typedef. It makes the header readable, gives the type a meaningful name (CompletionHandler, TableViewCellConfigurator), and ensures the signature is consistent across the API.

copy for block properties

Block literals start life on the stack. To keep a block alive past the scope that created it (storing it in a property, an ivar, a collection), you must move it to the heap with copy. Under ARC, a @property (copy) declared on a block type does this automatically — always use copy, never strong, for block properties:

objc
@property (nonatomic, copy) void (^onComplete)(void);   // correct — copies to heap
@property (nonatomic, strong) void (^onComplete)(void); // wrong — may be a dangling stack block

Capturing variables

A block captures the variables visible in the scope where it is created. By default it captures by value — a snapshot of the variable at the moment the block is created:

objc
int counter = 0;
void (^bump)(void) = ^{
    NSLog(@"%d", counter);   // captured value: 0
};
counter = 100;                // reassign the outer variable
bump();                       // prints 0, not 100

__block for mutable capture

If the block needs to modify the captured variable (not just read a snapshot), mark the variable __block. This moves the variable into a heap-allocated box that both the block and the surrounding scope share:

objc
__block int total = 0;
NSArray *counts = @[@1, @2, @3];
[counts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    total += [obj integerValue];     // modifying the captured variable
}];
NSLog(@"%d", total);                 // 6

Without __block, the line total += ... is a compiler error — you cannot assign to a by-value-captured variable.

Captured objects are retained

When a block captures an Objective-C object pointer, it retains that object (a __strong capture by default). The object stays alive at least as long as the block does. This is the mechanism behind the retain-cycle problem below — and it is why __block on an object pointer also retains it (older code sometimes used __block to avoid capture-retain; under ARC that no longer works — use __weak).

The weak/strong dance

This is the #1 blocks gotcha and the most common bug in ObjC codebases. Page 04 introduced it; here is the full picture.

If a block captures self strongly, and self holds the block strongly (directly or indirectly), you have a retain cycle — neither can ever dealloc:

objc
// BUG: retain cycle
self.onComplete = ^{
    [self saveData];   // block retains self; self retains block → leak
};

The fix is to capture self weakly, then re-strengthen it inside the block so it cannot dealloc mid-execution:

objc
__weak typeof(self) weakSelf = self;
self.onComplete = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return;            // self already deallocated before the block ran
    [strongSelf saveData];              // safe: strongSelf is alive for the block's scope
};

Step by step:

  1. __weak typeof(self) weakSelf = self; — a non-owning reference. It does not keep self alive, so no cycle. If self deallocs, weakSelf is auto-set to nil.
  2. __strong typeof(weakSelf) strongSelf = weakSelf; — a temporary strong reference taken when the block runs. It keeps self alive for the duration of the block so it cannot vanish mid-method.
  3. if (!strongSelf) return; — mandatory. Between capture and execution, self may have dealloced. Sending messages to nil is safe in ObjC, but you usually want to bail out rather than run half a method.
  4. typeof(self) resolves to the actual class type at compile time, so the code is subclass-safe and you keep full type information on strongSelf.

When do you actually need the dance?

Only when the block is retained by self (a property, an ivar, a strong path). A block passed to a short-lived API and not stored — [UIView animateWithDuration:animations:], a one-shot dispatch_async to the main queue that doesn't capture self into storage — does not need it, because there's no cycle. When in doubt, use it; the cost is two lines and a nil check.

GCD — Grand Central Dispatch with blocks

GCD is Apple's C-level concurrency API, and its entire interface is block-based. The two calls you will use most:

objc
// Run work on a background queue:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    NSData *data = [NSData dataWithContentsOfURL:url];   // blocking I/O off the main thread
    NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

    // Hop back to the main thread to touch the UI:
    dispatch_async(dispatch_get_main_queue(), ^{
        self.label.text = text;
    });
});
  • dispatch_async(queue, block) — schedules block on queue and returns immediately; the block runs later on that queue's thread.
  • dispatch_get_main_queue() — the serial queue tied to the main thread. All UI work must happen here.
  • dispatch_get_global_queue(QOS_CLASS_…, 0) — a shared concurrent background queue, classified by quality-of-service (QOS_CLASS_USER_INITIATED, QOS_CLASS_UTILITY, etc.).

The weak/strong dance applies here too: the background block captures self (via self.label), so if self could dealloc before the block runs, capture weakly first.

The completion handler pattern

The standard asynchronous API shape in ObjC is a method that takes a completion block and calls it when the work is done — usually on a background queue. Here is a complete, correct example:

objc
// NetworkService.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^FetchCompletion)(NSData * _Nullable data, NSError * _Nullable error);

@interface NetworkService : NSObject

- (void)fetchURL:(NSURL *)url completion:(FetchCompletion)completion;

@end

NS_ASSUME_NONNULL_END
objc
// NetworkService.m
#import "NetworkService.h"

@implementation NetworkService

- (void)fetchURL:(NSURL *)url completion:(FetchCompletion)completion {
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        NSError *error = nil;
        if (!data) {
            error = [NSError errorWithDomain:@"NetworkService"
                                        code:-1
                                    userInfo:@{NSLocalizedDescriptionKey: @"fetch failed"}];
        }

        // Callers expect completion on the main thread — hop back.
        dispatch_async(dispatch_get_main_queue(), ^{
            completion(data, error);
        });
    });
}

@end

And the call site, with the weak/strong dance because the view controller holds itself alive long enough to receive the result:

objc
// MyViewController.m
#import "MyViewController.h"
#import "NetworkService.h"

@implementation MyViewController

- (void)loadData {
    NetworkService *service = [[NetworkService alloc] init];
    NSURL *url = [NSURL URLWithString:@"https://example.com/data.json"];

    __weak typeof(self) weakSelf = self;
    [service fetchURL:url completion:^(NSData *data, NSError *error) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) return;

        if (error) {
            [strongSelf showError:error];
            return;
        }
        [strongSelf renderData:data];
    }];
}

- (void)renderData:(NSData *)data { /* ... */ }
- (void)showError:(NSError *)error { /* ... */ }

@end

Notice the pattern is self-similar with the delegate example from page 05, but inverted: a delegate wires up a long-lived relationship, while a completion handler is a one-shot callback. Modern Swift replaces both with async/await and Codable, but the underlying machinery — hop to a background queue, do the work, hop back to main, deliver the result — is identical.

Blocks vs Swift closures

Objective-CSwift
void (^block)(void)() -> Void
^(NSString *name){ ... }{ name in ... }
typedef void (^Completion)(NSData *, NSError *);typealias Completion = (Data, Error) -> Void
__block int x (mutable capture)var x captured by reference automatically
__weak typeof(self) weakSelf = self; + strong dance[weak self] in capture list + guard let self else { return }
@property (copy) CompletionHandler cb;var cb: Completion? (closures are reference types, escaped automatically)
dispatch_async(queue, ^{...})DispatchQueue.global().async { ... }

The biggest win moving to Swift: the weak/strong dance collapses into a one-line capture list ([weak self]), and mutable capture is the default for var bindings — no __block keyword. But the underlying concept (capture semantics, retain cycles, hopping queues) is identical, so understanding blocks pays off in Swift too.

What's next

You can now write asynchronous, callback-driven code. The next page brings everything together on the UI layer — UIKit — covering the view-controller lifecycle, target-action, IBOutlet/IBAction, and programmatic Auto Layout, all in Objective-C.

NextUIKit with ObjC