Objective-C
performSelector
blocks
async-programming
ios-development

Blocks instead of performSelectorwithObjectafterDelay

Master System Design with Codemia

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

Introduction

In modern Objective-C, blocks are usually a better choice than performSelector:withObject:afterDelay: for delayed execution. The usual replacement is dispatch_after, because it is clearer, more flexible, and better suited to passing context than the older selector-based API.

Why the Selector-Based API Feels Limited

The classic delayed-selector pattern still works:

objective-c
1- (void)startWork {
2    [self performSelector:@selector(showMessage:) withObject:@"done" afterDelay:2.0];
3}
4
5- (void)showMessage:(NSString *)message {
6    NSLog(@"%@", message);
7}

But the design has several drawbacks:

  • the delayed action is split across two methods
  • only one object argument is passed directly
  • the selector itself is less expressive than inline code
  • scheduling behavior is tied to older run-loop style thinking

For small delays this may be fine, but once the action needs more context, queue choice, or cancellation logic, blocks become easier to maintain.

Use dispatch_after for the Modern Block-Based Pattern

The common replacement is Grand Central Dispatch:

objective-c
1#import <dispatch/dispatch.h>
2
3- (void)startWork {
4    dispatch_time_t delay =
5        dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
6
7    dispatch_after(delay, dispatch_get_main_queue(), ^{
8        NSLog(@"done");
9    });
10}

This expresses the delay, target queue, and delayed action in one place. That makes the flow easier to read because you do not have to jump between a selector call and a separate callback method just to understand what will happen later.

For UI work, the main queue is the correct choice. For background work, choose a background queue explicitly.

Blocks Make Passing Context Much Easier

One of the biggest practical benefits of blocks is that they capture local variables naturally.

objective-c
1- (void)notifyUser:(NSString *)name {
2    NSString *message = [NSString stringWithFormat:@"Hello, %@", name];
3
4    dispatch_time_t delay =
5        dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
6
7    dispatch_after(delay, dispatch_get_main_queue(), ^{
8        NSLog(@"%@", message);
9    });
10}

With performSelector, you would often need an extra method just to carry one delayed value. With a block, the surrounding state is already available. That becomes even more valuable when the delayed logic needs several local values.

Queue control is also more explicit:

objective-c
1dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
2dispatch_after(delay, queue, ^{
3    NSLog(@"background work finished");
4});

That is harder to express cleanly with the old selector API.

Design Cancellation and Ownership Explicitly

One advantage of the old delayed-selector approach was the paired cancellation API. With blocks, cancellation is not automatic just because you used dispatch_after. If cancellation matters, represent the work item directly:

objective-c
1@property (nonatomic, copy) dispatch_block_t pendingBlock;
2
3- (void)scheduleTask {
4    dispatch_block_t block = dispatch_block_create(0, ^{
5        NSLog(@"runs later unless cancelled");
6    });
7
8    self.pendingBlock = block;
9
10    dispatch_time_t delay =
11        dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
12
13    dispatch_after(delay, dispatch_get_main_queue(), block);
14}
15
16- (void)cancelTask {
17    if (self.pendingBlock) {
18        dispatch_block_cancel(self.pendingBlock);
19    }
20}

Also remember that blocks capture objects strongly by default. If the delayed block may outlive the current interaction, a weak reference to self can prevent retain cycles:

objective-c
1__weak typeof(self) weakSelf = self;
2
3dispatch_after(delay, dispatch_get_main_queue(), ^{
4    [weakSelf refreshUI];
5});

This is not unique to dispatch_after, but block capture rules become part of the design once you switch away from selectors.

Common Pitfalls

The biggest mistake is replacing performSelector with blocks mechanically while ignoring queue choice. UI updates still belong on the main queue, and background work should not touch UIKit directly.

Another issue is assuming dispatch_after is automatically cancellable. If cancellation matters, create and manage a cancellable block or choose a timer abstraction that matches the problem better.

Developers also sometimes overlook retain cycles when a delayed block captures self strongly and is also retained by that same object.

Finally, if the real problem is debouncing repeated events or scheduling recurring work, a timer or a dedicated scheduling abstraction may fit better than stacked delayed blocks.

Summary

  • Blocks are usually a cleaner replacement for performSelector:withObject:afterDelay:.
  • 'dispatch_after is the standard modern Objective-C tool for delayed execution.'
  • Blocks are easier to read because they keep the delayed action close to the scheduling call.
  • They also capture local context naturally, which makes delayed code more flexible.
  • If you need cancellation or careful ownership, design that explicitly when moving to a block-based approach.

Course illustration
Course illustration

All Rights Reserved.