Stupid Swift Tricks #2
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:
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.
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:
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.
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:
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.
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:
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:
@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...
}
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:
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:
// 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:
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.