ReactiveCocoa and cancellable delayed blocks

Using ReactiveCocoa in your projects can bring up some really nice solutions. I’m using it a lot and I’m creating more and more custom RACSignals for cool solutions to problems. One of these solutions is cancellable delayed blocks

cancellable delayed blocks

Sometimes you want to create delayed blocks. Blocks which will fire after a certain time interval. That’s one thing, which should be quite easy to create with the default API methods. But what if you want to cancel that block of code if something happens within the time interval?

You could use throttle for that with ReactiveCocoa, but then you need a signal which is sending an object without direct completion. My solution here is slightly different, containing some helper methods I created earlier.

#1: RACSignal return in Swift

ReactiveCocoa comes with the return method for sending an object directly through a signal. It’s working great in Objective-c, but wont work in Swift. My solution is an Objective-c category:

@interface RACSignal (ReturnObject)

+ (RACSignal *)returnObject:(id)object;

@end

@implementation RACSignal (ReturnObject)

+ (RACSignal *)returnObject:(id)object {
    return [RACSignal return:object];
}

@end

This makes it possible to send an object through a signal:

RACSignal.returnObject("Sample string").subscribeNextAs { (string:String) -> () in
    println(string) // prints: "Sample string"
}

#2: Execute with delay

As throttle wont work with the above returnObject method, we need to execute our signal with a delay. This is possible using the following Swift extension:

extension RACSignal {
    func execute() -> RACDisposable {
        return self.subscribeCompleted { () -> Void in }
    }

    func executeWithDelay(interval:NSTimeInterval) -> RACDisposable {
        var signals = [RACSignal.empty().delay(interval), self]
        var delayedSignal = RACSignal.concat(signals)
        return delayedSignal.execute()
    }
}

Notice my naming convention for subscribeCompleted. As I sometimes just want to execute a signal without the completion block, I’ve created this execute() method which looks a lot nicer without that empty block everywhere.

The delayed execution is simple:

mySignal.execeuteWithDelay(10) // Will fire the signal after 10 seconds

Bringing it all together

Combining these methods can give you the cancellable delayed blocks method. It looks as followed:

extension RACSignal {
    class func performBlock(block:() -> Void, afterDelay delay:NSTimeInterval) -> RACDisposable {
        return RACSignal.returnObject("").doNext({ (_) -> Void in
            block()
        }).executeWithDelay(delay)
    }
}

And gives you a cancellable disposable object!

In practice it looks like this:

class ViewController: UIViewController {

    private var sayHiDisposable:RACDisposable?

    @IBAction func sendHiWithDelayButtonTapped(sender: UIButton) {
        sayHiDisposable?.dispose()
        sayHiDisposable = RACSignal.performBlock({ () -> Void in
            println("Saying hi!")
        }, afterDelay: 3.0)

    }   
}

Hitting the button for 100 times, will just delay the execution as the previous one gets cancelled each time. Finally it will print one line of “Saying hi!”.

Real example

This works for me in some scenario’s. I recently used this method for my scrubbing method inside the videoplayer. I wanted to trigger a ‘play’ event after seeking completed. As I didn’t get a scrubbing completed callback, I decided to send the play event after a delay of 2.5 seconds. But if the user scrubbed again within these 2.5 seconds, this block had to be cancelled. Therefor!

 

Antoine van der Lee

Dutch iOS developer at Triple. Developed apps like Buienradar, Videoland and Pop the Dots.

 
Follow on Feedly