Taking Notes

cause i'll forget

Coalescing

One of my favorite staples of Cocoa is –performSelector:withObject:afterDelay:. It’s a dead simple ‘do this later’ implementation and endlessly useful for scheduling simple tasks.

One of the things I use it for the most is to coalesce multiple calls into one call. A simple example would be reloading a table view everytime new data becomes available. If you’re getting new data 100 times a second, you don’t really want to reload the table every time.

Here’s a contrived example with a category on NSObject to formalize coalescing:

@interface NSObject (PerformCalescing)
- (void)rstl_coalescedPerformSelector:(SEL)sel;
@end
@implementation NSObject (Perform)
- (void)rstl_coalescedPerformSelector:(SEL)sel
{
    // Cancel any previous perform requests to keep calls from piling up.
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:sel object:nil];
    // Schedule the call, it will hit during the next turn of the current run loop
    [self performSelector:sel withObject:nil afterDelay:0.0];
}
@end
@interface Foo : NSObject
@end

@implementation Foo
- (void)foo
{
    NSLog(@"Foo");
}
@end

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        Foo *foo = [Foo new];
        [foo rstl_coalescedPerformSelector:@selector(foo)];
        [foo rstl_coalescedPerformSelector:@selector(foo)];
        [foo rstl_coalescedPerformSelector:@selector(foo)];
        CFRunLoopRun();
    }
    return 0;
}

If you ran this example, you’d see it only prints Foo once:

2013-01-03 19:37:50.560 Test[6405:303] Foo

A less contrived example might be something more like this:

@interface Foo : UIView
- (void)setNeedsRefresh;
- (void)refresh;
@property (nonatomic) bool refreshRelatedProperty;
@property (nonatomic) bool otherRefreshRelatedProperty;
@end

@implementation Foo
- (void)setRefreshRelatedProperty:(bool)refreshRelatedProperty
{
    if (refreshRelatedProperty != _refreshRelatedProperty)
    {
        _refreshRelatedProperty = refreshRelatedProperty;
        [self setNeedsRefresh];
    }
}
- (void)setOtherRefreshRelatedProperty:(bool)otherRefreshRelatedProperty
{
    if (otherRefreshRelatedProperty != _otherRefreshRelatedProperty)
    {
        _otherRefreshRelatedProperty = otherRefreshRelatedProperty;
        [self setNeedsRefresh];
    }
}
- (void)setNeedsRefresh
{
    [self rstl_coalescedPerformSelector:@selector(refresh)];
}
- (void)refresh
{
    NSLog(@"Refresh");
}
@end

...
foo.refreshRelatedProperty = true;
foo.otherRefreshRelatedProperty = true;
...

In this case you could assume that Foo is a user interface object of some sort and that a real implementation of refresh would do something expensive like draw or update a layer. I couldn’t speak to the actual implementation, but this is the basic design of -setNeedsDisplay and -setNeedsLayout on UIView.

This API is run loop based, so you won’t be able to use it on detached threads unless you manually run that threads run loop. In practice, it’s not an issue that comes up a lot.


At this point, some of you may have noticed the object argument on cancel and perform that is left out of the category above.

Something like:

@interface NSObject (PerformCalescing)
- (void)rstl_coalescedPerformSelector:(SEL)sel withObject:(id)obj;
@end
@implementation NSObject (Perform)
- (void)rstl_coalescedPerformSelector:(SEL)sel withObject:(id)obj
{
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:sel object:obj];
    [self performSelector:sel withObject:obj afterDelay:0.0];
}
@end

The answer to why that is the case is in the docs for +cancelPreviousPerformRequestsWithTarget:selector:object:

The argument for requests previously registered with the performSelector:withObject:afterDelay: instance method. Argument equality is determined using isEqual:, so the value need not be the same object that was passed originally. Pass nil to match a request for nil that was originally passed as the argument.

The requirement that the objects passed match based on -isEqal: doesn’t lend itself well to coalescing. You aren’t likely to call the same reload method with the same object again and again. (If you are doing this, then you likely have made some questionable design choices.)


Couple of things you could play around with: