iOS: Throttling a piece of code that’s called way too often

This is a quick post because today is a workday, but I wanted to share this snippet in case anyone finds it useful. Take, for example, that you have a color picker, and you want to update a live OpenGL rendering view every time the color changes to have it reflect the color the user chose. This is great and all, but if the user drags along the color picker view and it sends out 120 “color changed” updates per second, it’s an unnecessary drain on resources.

This allows you to write code like:

self.colorPickerView.colorChangedBlock = BlockThrottler.throttleBlock(interval: 0.25) { [weak self] color in
// do things with the color
}

Don’t forget your weak self capture! Here’s the full class:

class BlockThrottler<T> {
    typealias Block = (T) -> Void
    var block: Block
    var throttleInterval: NSTimeInterval
    weak var originalQueue: dispatch_queue_t?
    var lastArg: T?
    
    private init(block: Block, throttleInterval: NSTimeInterval) {
        self.block = block
        self.throttleInterval = throttleInterval
        
        
        if let queue = NSOperationQueue.currentQueue() {
            self.originalQueue = queue.underlyingQueue
            self.callBlock()
        } else {
            assert(false, "no current queue")
        }
    }
    
    func callBlock() {
        if let arg = self.lastArg {
            self.block(arg)
            self.lastArg = nil
        }
        
        if let queue = self.originalQueue {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(self.throttleInterval * NSTimeInterval(NSEC_PER_SEC))), queue) { [weak self] in
                self?.callBlock()
            }
        }
    }
    
    func callableBlock() -> Block {
        return { arg in // strongly capture self since we need to exist for the lifetime of the block
            self.lastArg = arg
        }
    }
    
    class func throttleBlock(interval throttleInterval: NSTimeInterval, block: Block) -> Block {
        return BlockThrottler(block: block, throttleInterval: throttleInterval).callableBlock()
    }
}

 

Here are some possible extensions off the top of my head.

Throttling a delegate method:

// in a class
let colorChangedThrottler = BlockThrottler<UIColor>({ color in
  // do things with the color; meat of the method is here
}, 0.25)
/*delegate*/ func colorChanged(color: UIColor) {
  colorChangedThrottler.lastArg = color
  // maybe you would want to define a method in BlockThrottler like queueCall(arg: T) to make this look a bit better
}

// alternatively, but still pretty ugly
var colorChangedThrottler: BlockThrottler<UIColor>?
/*delegate*/ func colorChanged(color: UIColor) {
  self.colorChangedThrottler = self.colorChangedThrottler ?? BlockThrottler<UIColor>({ color in 
    // now the meat of the method is in the method itself
  }, 0.25)
  self.colorChangedThrottler?.lastArg = color
}

More or less than a single variable passed into the block:

This one’s pretty straightforward, here’s a theoretical implementation of a 2-arg one (notice how I added “U” and “lastArg2”), and a no-arg one (notice how I changed lastArg to a Bool):

class TwoArgBlockThrottler<T, U> {
    typealias Block = (T, U) -> Void
    var block: Block
    var throttleInterval: NSTimeInterval
    weak var originalQueue: dispatch_queue_t?
    var lastArg: T?
    var lastArg2: U?
    
    init(block: Block, throttleInterval: NSTimeInterval) {
        self.block = block
        self.throttleInterval = throttleInterval
        
        
        if let queue = NSOperationQueue.currentQueue() {
            self.originalQueue = queue.underlyingQueue
            self.callBlock()
        } else {
            assert(false, "no current queue")
        }
    }
    
    func callBlock() {
        if let arg = self.lastArg, arg2 = self.lastArg2 {
            self.block(arg, arg2)
            self.lastArg = nil
            self.lastArg2 = nil
        }
        
        if let queue = self.originalQueue {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(self.throttleInterval * NSTimeInterval(NSEC_PER_SEC))), queue) { [weak self] in
                self?.callBlock()
            }
        }
    }
    
    func callableBlock() -> Block {
        return { arg, arg2 in // strongly capture self since we need to exist for the lifetime of the block
            self.lastArg = arg
            self.lastArg2 = arg2
        }
    }
    
    class func throttleBlock(interval throttleInterval: NSTimeInterval, block: Block) -> Block {
        return TwoArgBlockThrottler(block: block, throttleInterval: throttleInterval).callableBlock()
    }
}
class NoArgBlockThrottler<T, U> {
    typealias Block = () -> Void
    var block: Block
    var throttleInterval: NSTimeInterval
    weak var originalQueue: dispatch_queue_t?
    var shouldCall = false
    
    init(block: Block, throttleInterval: NSTimeInterval) {
        self.block = block
        self.throttleInterval = throttleInterval
        
        
        if let queue = NSOperationQueue.currentQueue() {
            self.originalQueue = queue.underlyingQueue
            self.callBlock()
        } else {
            assert(false, "no current queue")
        }
    }
    
    func callBlock() {
        if self.shouldCall {
            self.block()
            self.shouldCall = false
        }
        
        if let queue = self.originalQueue {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(self.throttleInterval * NSTimeInterval(NSEC_PER_SEC))), queue) { [weak self] in
                self?.callBlock()
            }
        }
    }
    
    func callableBlock() -> Block {
        return { // strongly capture self since we need to exist for the lifetime of the block
            self.shouldCall = true
        }
    }
    
    class func throttleBlock(interval throttleInterval: NSTimeInterval, block: Block) -> Block {
        return NoArgBlockThrottler(block: block, throttleInterval: throttleInterval).callableBlock()
    }
}

Note, I haven’t tested any of these extensions, just wrote them up for completeness. Use at your own peril.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s