• Wooji Juice
  • Blog
  • Support
  • About

Stupid Swift Tricks #2

Coalescing Updates

Update: A few parts of this post are affected by Swift 1.2. Once it’s out of beta, I’ll do a rewrite to reflect the changes, but in the meantime, I’ve collected the updates together at the end.

Here’s another little toy I’ve been using in Swift. Essentially, it’s a wrapper around dispatch_after() that defers execution of a block of code for a given amount of time. Except that if, when it goes to execute the block, you’ve already scheduled another, it throws the first away:

Swift

Coalesce function
private var coalesceTracker: [String:Int] = [:] func coalesce(context: String, # timeout: NSTimeInterval, # queue: dispatch_queue_t, block: ()->()) { let mainQueue = dispatch_get_main_queue() if !NSThread.isMainThread() { dispatch_async(mainQueue) { coalesce(context, timeout: timeout, queue: queue, block) } return } coalesceTracker[context] = (coalesceTracker[context] ?? 0) + 1 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(timeout * NSTimeInterval(NSEC_PER_SEC))), mainQueue) { let trackingCode = (coalesceTracker[context] ?? 1) - 1 if trackingCode == 0 { coalesceTracker[context] = nil if queue === mainQueue { block() } else { dispatch_async(queue, block) } } else { coalesceTracker[context] = trackingCode } } } func coalesce(context: String, # timeout: NSTimeInterval, block: ()->()) { coalesce(context, timeout: timeout, queue: dispatch_get_main_queue(), block) } func coalesce(context: String, block: ()->()) { coalesce(context, timeout: 0, block) }

(You may be wondering, why define overrides to coalesce() just to set default parameters? Doesn’t Swift support default parameters? Well, yes, it does… but doing that breaks trailing closure syntax. And I like trailing closure syntax.)

Calls are scoped with a context, so you can have multiple coalesce() calls going on in different parts of the code without them interfering with each other. Different calls can have different time delays, and be scheduled in different orders, and it’s all good – only the one that’s scheduled to execute last in wall-clock time, actually gets executed.

Okay, But Why?

The purpose is to avoid repeatedly performing certain tasks. For example, if you have some expensive calculations that depend on a value set by the user, and they’re dragging a widget around to set that value, it might be too expensive to recalculate every time you receive a touch event. There’s a continuous stream of them, and responding to every one will cause lag – even if you recalculate on a background queue, because the queue fills up with obsolete calculations.

On the other hand, waiting for the “touch up” event (indicating the drag has ended) sucks because you lose any sense of live interaction. A good tradeoff is to coalesce touch events, with a short timeout:

Swift

Handling the touch event
func dragged(gesture: UIPanGestureRecognizer) { switch gesture.state { case .Changed: let value = gesture.locationInView(self).x / bounds.width coalesce("drag", timeout: 0.1) { self.performExpensiveUpdateWith(value) } default: break } }

If the user ends the drag, or pauses for 1/10th of a second, they get an update. While their finger is in motion, updates are held back. coalesce() can be passed a queue to execute blocks on, so you can run updates in the background. It defaults to the main thread since most often it’s being triggered by, and needs to do work with, UIKit.

The Catch

Now, there is potentially a subtle bug lurking in the example above. The problem is the token "drag": you should never actually pass a string constant, unless you’re thoroughly damn sure there’s only ever one instance of your class. Otherwise, multiple instances will clash with each other since they share a token – you need to make it truly unique.

In some cases, I use a property of the instance – for example, I’m working on a project which uses coalesce() in a database, and there are multiple databases, but it ensures there’s only ever one instance of each database at a time. So, it uses the database path to uniquify the key.

The rest of the time, turns out, I seem to always be using this inside views and view controllers. Which ultimately derive from NSObject. So I make use of this:

Swift

Extension to generate unique tokens
extension NSObject { func uniqueToken(key: String) -> String { let uniqueid = Unmanaged<AnyObject>.passUnretained(self).toOpaque() return "\(key):\(uniqueid)" } func uniqueToken(file: String = __FILE__, line: Int = __LINE__) -> String { let uniqueid = Unmanaged<AnyObject>.passUnretained(self).toOpaque() return "\(file):\(line):\(uniqueid)" } }

So the token gets uniqued by instance and by either call-site (convenient) or key (where multiple call-sites need to share a token). The odd-looking stuff to create uniqueid is simply a way to get self into the string as an address – simply interpolating self into the string directly isn’t guaranteed to return a unique result.

Immediate Coalescing

Back to coalesce() itself: If you don’t specify a timeout, then the block is executed at the end of the current run loop (or as close as possible), which is useful for implementing setNeedsDisplay()-style behaviour. If you have a bunch of properties that, when updated, need to trigger a recalculation, and you don’t want to recalculate a bunch of times if a set of properties update at once, you can do this:

Swift

"Immediate" Coalescing
class SynthesiserEnvelope: NSObject { var attack: Float { didSet { setNeedsRender() }} var decay: Float { didSet { setNeedsRender() }} var sustain: Float { didSet { setNeedsRender() }} var release: Float { didSet { setNeedsRender() }} func setNeedsRender() { coalesce(uniqueToken()) { doExpensiveRender() } } func doExpensiveRender() { // ... } } let adsr = SynthesiserEnvelope() adsr.attack = 0.1 adsr.decay = 0.2 adsr.sustain = 0.5 adsr.release = 2 // doExpensiveRender() only gets called once, while keeping // SynthesiserEnvelope's API very straightforward & obvious

Of course, this is only appropriate if you’re not immediately going to be doing work that depends on the results of the render, since it happens asynchronously. It’s not a panacea, but for a wide variety of tasks – typically based around UI updates – it’s useful. It makes a good replacement for situations where you might’ve been doing the performSelector:withObject:afterDelay: and cancelPreviousPerformRequestsWithTarget:selector:object: dance in the past.

I’m pretty happy with this. I’d be happier if I could find a way to make the call to uniqueToken() happen automagically, so that one could simply write: coalesce { doExpensiveRender() }, making the construct look as much as possible like a native part of the language, while retaining safety between multiple instances. Unfortunately this doesn’t seem to be possible, since any solution seems to rely on default parameters (initialised to __FILE__ & __LINE__) and as we’ve noted, those break trailing closure syntax. If you figure out a way, do let me know…


Anyway, that’s all for now. I have more Stupid Swift Tricks coming up soon, though. I’ve noticed a lot of interest from various people in Swift implementations of KVO, notifications and the like. I have my own hat to thrown into the ring on this one, but I have a few things to finish first. Still, here’s a sneak previous of what I’ve been up to:

Swift

Sneak Preview
@IBOutlet weak var mySwitch: UISwitch! @IBOutlet weak var myTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() // Binding values Bind(myTextField) --> { println("Text is: \($0)") } // Value transformers Bind(mySwitch).on --> { !$0 } --> Bind(myTextField).enabled // Notifications let device = UIDevice.currentDevice() Bind(device).Notification(UIDeviceBatteryStateDidChangeNotification) --> { notification in if device.batteryState == .Full { println("Your \(device.localizedModel) has fully charged.") println("Go forth and be free of your electrical tether!") } } // And much more... }

Update: Coalesce in Swift 1.2

Swift 1.2 fixes the behaviour of default arguments with respect to trailing closure syntax. This means we can now replace the previous overloaded functions with a version of coalesce() written like this:

Swift

Coalesce improvements
func coalesce(context: String, timeout: NSTimeInterval = 0, queue: dispatch_queue_t = dispatch_get_main_queue(), block: ()->()) { // ... the rest as before ... }

We can also streamline things with respect to the unique tokens. For example:

Swift

Unique token improvements
// First, a trampoline so we can call from the shadowed method in NSObject private func coalesceTrampoline(context: String, timeout: NSTimeInterval, queue: dispatch_queue_t, block: ()->()) { coalesce(context, timeout: timeout, queue: queue, block) } // Then extensions so that coalesce is safe inside classes extension NSObject { func coalesce(timeout: NSTimeInterval = 0, queue: dispatch_queue_t = dispatch_get_main_queue(), file: String = __FILE__, line: Int = __LINE__, block: ()->()) { let uniqueid = Unmanaged<AnyObject>.passUnretained(self).toOpaque() coalesceTrampoline("\(file):\(line):\(uniqueid)", timeout, queue, block) } func coalesce(key: String, timeout: NSTimeInterval = 0, queue: dispatch_queue_t = dispatch_get_main_queue(), block: ()->()) { let uniqueid = Unmanaged<AnyObject>.passUnretained(self).toOpaque() coalesceTrampoline("\(key):\(uniqueid)", timeout, queue, block) } }

Now, inside a view controller or other NSObject subclass, you can simply write:

Swift

Calling Coalesce
coalesce { /* do stuff */ } // or coalesce(timeout: 0.1) { /* do stuff */ }

Without needing to specify any other clutter, and it’s safe with respect to multiple instances and call sites.

Posted

by Canis

6 February 2015

Updated 18 February 2015

Contact

Email

Social

Archives

By Year

By Topic

© Wooji Juice 2025