radex.io

aboutarchiveetsybooksmastodontwitter

Swifty APIs: NSTimer

May 11, 2015
Hey! This is an old post. It reflects my views and the state of the world in 2015, but is likely to be outdated. You’ll find me on Twitter, building Nozbe, and making engineering blueprints.

In the previous installment of Swifty APIs, we took a look at NSUserDefaults and gave it a make-over. I pointed out some of its problems, like verbosity and inconsistent behavior, and tried to fix them. The resulting interface was, arguably, cleaner, easier to use, and felt more at home with Swift’s syntax.

Today on my plate is NSTimer. This class has been a pet peeve of mine for a while. Perhaps I’ve been using it more often than most people (fun fact: one of my apps is essentially a fancy, special-purpose timer), but I really do think that for the simple, almost trivial function it serves, its API seems unnecessarily inconvenient. It feels outdated not just by Swift standards — it stayed unchanged for at least 15 years, despite advancements in Objective-C.

Either way, let’s make it better.

The name

Here’s our victim:

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(foo:) userInfo:nil repeats:YES]

Our first order of business will be to change the name of this method to something slightly less verbose.

Why do we have to say “timer” in scheduledTimerWithTimeInterval? We just said “NSTimer”, what else could we possibly be scheduling? Why say “time interval”, when “interval” would do? Again, it’s a timer, of course the interval is about time. And the word “with” is pure syntactic noise. It explains nothing to the reader. How about this:

NSTimer.schedule(interval: 1.0, ...)

It’s a subtle difference, of course, but by making things shorter, we’re reducing the volume of code we have to read and understand. Brevity for its own sake is not the point. The point is to remove noise — words and constructs that take up space, but convey very little information. We’re trying to distill our code to only things that actually matter.

(This is not an easy topic that I can explain in one paragraph. I encourage you to read Swifty methods where I dig deep into the nuances of naming things in search of clarity.)

Closing over our target

Let’s see how we’re doing:

NSTimer.schedule(interval: 1.0, target: self, selector: "hello", userInfo: nil, repeats: true)

@objc func hello() {
    println("Hello world!")
}

Ouch. Look at all that code. We might have shortened the name, but there’s still a lot of work to do.

The most frustrating thing here is that we have to define a new method just to schedule a timer. That seems unreasonable to me. Yes, sometimes you’d want to do that anyway, but often times I just want to quickly debug something or run a few lines of code with a timer.

You know where this is going. Let’s get rid of the target/selector pair and replace it with a closure.

NSTimer.schedule(interval: 1.0, userInfo: nil, repeats: true) {
    println("Hello world!")
}

So easy, and so much better.

Passing data

There’s a very good chance you know what userInfo is. But in case you don’t, it’s a simple mechanism of passing data between methods. It works like this:

NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "hello:", userInfo: "radex.io", repeats: false)

@objc func hello(timer: NSTimer) {
    let message = timer.userInfo!
    println("Hello from \(message)") // prints "Hello from radex.io"
}

We don’t need that anymore, though. The whole beauty of closures is that, in addition to defining a function inline, they can access variables in the defining scope.

func delayedHello(message: String) {
    NSTimer.schedule(interval: 1.0, repeats: true) {
        println("Hello from \(message)")
    }
}

Closures are such a superior solution to target, selector & userInfo. If we want to run just a few lines of code, we can do that inline. If we want to call existing method with arguments, we can define a one-line closure and pass parameters from the defining scope. And if we want to call a method without any arguments, we can simply pass a reference to that method instead of a closure — Swift treats them as equivalent.

Delaying and repeating

With the use of closures, we’ve gotten to a pretty good shape. We designed a perfectly respectable API, and we could very well leave it at that.

But let’s try something different. Instead of passing the repeats parameter, how about we make two separate methods, for repeating and non-repeating timers:

NSTimer.every(interval: 1.0) {
    println("Another second elapsed...")
}

NSTimer.after(interval: 5.0) {
    println("Just a one-time delay!")
}

I like this. It makes a lot of sense for me to make those two different methods. Under the hood, timer repetition might be just a parameter. But from the API user point of view, I see it as two different jobs to be done. Sometimes I want to delay something, and sometimes I want to repeat something. I’ve never encountered a situation where I would actually want to pass a variable to repeats, and not just a true/false constant.

And hey, we also made these invocations a bit shorter. Again, not our main goal, but it certainly doesn’t hurt.

Clearer time intervals

The purpose of Swifty APIs isn’t just to make something cool and take advantage of Swift’s features. There’s some of it (because why not) but the ultimate goal is to produce an API that’s easier to use and understand. We want the interface to be readable and clear.

And as far as clarity is concerned, one thing that bothers me is how we express time intervals. I mean, what’s 5.0? Five of what? You can usually infer from context that it is a time interval, and you can probably guess that it’s measured in seconds… But I think we can do better. We can make this clearer, without being too verbose.

Here’s an idea I stole from Ruby on Rails:

NSTimer.every(1.minute) {
    println("Another minute elapsed...")
}

NSTimer.after(5.seconds) {
    println("Do you want to rate us on App Store?")
}

Wow. Isn’t this… cute? But it’s so clear, readable, and expressive, too!

It sounds almost like English: “every 1 minute”, “after 5 seconds”. Usually when people try to make a programming language read like English, they make it worse, and you end up with something that’s harder, not easier to understand. But not here. We just got ourselves a natural-sounding and expressive API that everyone will immediately understand, yet it’s almost as terse as it can possibly be.

Implementation

Now that we’ve designed an alternative interface for NSTimer, it’s time to write the code.

Let’s start with the hardest part — closures. Remember, NSTimer requires a target object and a method on that object to run. Swift functions are not objects, so we’ll need to introduce a helper class to wrap the closure.

private class NSTimerActor {
    var block: () -> ()

    init(_ block: () -> ()) {
        self.block = block
    }

    @objc func fire() {
        block()
    }
}

This is a bridge between the closure-based API and the target/selector reality. We can initialize this object with a closure, and then call fire to run it. (The fire method needs to be @objc to work with NSTimer.)

Now we can do:

let actor = NSTimerActor {
    println("Hello!")
}

NSTimer(timeInterval: 1.0, target: actor, selector: "fire", userInfo: nil, repeats: false)

In case you’re wondering about memory management, the rules are on our side. NSTimer keeps a strong reference to its target object, so as long as the timer is alive, the “actor” object will be alive, and so will our closure.

Great! Now let’s just package this together. But before we define every and after, let’s define timer initializers. Why? Sometimes you don’t want your timer to be immediately scheduled. And there are some situations where you want to add a timer to the run loop yourself.

extension NSTimer {
    class func new(after interval: NSTimeInterval, _ block: () -> ()) -> NSTimer {
        let actor = NSTimerActor(block)
        return self.init(timeInterval: interval, target: actor, selector: "fire", userInfo: nil, repeats: false)
    }

    class func new(every interval: NSTimeInterval, _ block: () -> ()) -> NSTimer {
        let actor = NSTimerActor(block)
        return self.init(timeInterval: interval, target: actor, selector: "fire", userInfo: nil, repeats: true)
    }
}

Wait, wait, wait… class func new? That’s not an initializer! No, it’s not, but unfortunately, as of Swift 1.2, there’s a bug (18720947) that will crash this code if we define it as a convenience initializer. Calling this new seems like a reasonable compromise.

Now we can define every and after in terms of new:

extension NSTimer {
    class func after(interval: NSTimeInterval, _ block: () -> ()) -> NSTimer {
        let timer = NSTimer.new(after: interval, block)
        NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
        return timer
    }

    class func every(interval: NSTimeInterval, _ block: () -> ()) -> NSTimer {
        let timer = NSTimer.new(every: interval, block)
        NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
        return timer
    }
}

And finally, the icing on the cake, let’s implement our NSTimeInterval helpers:

extension Double {
    var second:  NSTimeInterval { return self }
    var seconds: NSTimeInterval { return self }
    var minute:  NSTimeInterval { return self * 60 }
    var minutes: NSTimeInterval { return self * 60 }
    var hour:    NSTimeInterval { return self * 3600 }
    var hours:   NSTimeInterval { return self * 3600 }
}

There’s a fair amount of repetition here. But the goal of these helpers is to be expressive and natural. So we have singular and plural variants — this way we can naturally write 1.second, 5.minutes, 0.7.seconds. (Because Double is IntegerLiteralConvertible, we can use these properties on integers for free, without extending Int.)

Result

Here’s our new, freshly baked NSTimer API:

// Scheduling a delay
NSTimer.after(1.minute) {
    println("Are you still here?")
}

// Repeating an action
NSTimer.every(0.7.seconds) {
    statusItem.blink()
}

// Pass a method reference instead of a closure
NSTimer.every(30.seconds, align)

// Make a timer object without scheduling
let timer = NSTimer.new(every: 1.second) {
    println(self.status)
}

And that’s it! Another class simplified, modernized, swiftified.

If you like it, I published the full source code on GitHub: SwiftyTimer. If you’re using CocoaPods, you can include this library in your project by adding pod 'SwiftyTimer' to your Podfile.

I also encourage you to check out my previous Swifty API project with NSUserDefaults, and my article about naming things: Swifty methods.

If you have comments, complaints or ideas for improvement, please let me know on Twitter or make an issue on GitHub.

Published May 11, 2015. Last updated May 15, 2015. Send feedback.