It’s been a long while. Part of it is that I’ve been busy working on Hazel 3. And since I’m not interested in writing a book, I’m finding it hard to be motivated to keep posting here. Nonetheless, every so often I have to let something squirt out.
Today, we are going to talk about NSTimer. One thing you may or may not have noticed is that in certain cases where time is suspended (putting your machine to sleep) or altered (changing the clock), NSTimer has a tendency to try and compensate. For instance, say you set a timer to fire in an hour. You close the lid on your MBP and come back 30 minutes later. You’ll see that the fire date for the timer has adjusted to take into account the time it was asleep. Here’s a quickie diagram for those not quite following:
Now, this is great if you wanted a timer to fire an hour later in the machine’s conception of time. But there are times when you want it to fire on the actual date you set on it. Typical example is setting a timer for a calendar appointment. That time is an absolute point in the user’s timeline and you don’t want that timer to shift to compensate for the machine’s timeline.
The basic fix here is when one of these time-altering events occurs, you reset the fire time on the timer to the original time that was set on it when it was created. Luckily, there are notifications for when the machine wakes from sleep as well as when the system clock is changed (this one being new in 10.6). Naturally, one would think to make an NSTimer subclass as the most straightforward way to do this. Unfortunately, it seems that it doesn’t work. Even if you get over the hurdle that NSTimer is actually abstract (like a class cluster), you have to add the timer to the run loop to schedule it. While NSRunLoop seems to accept the NSTimer subclass, it doesn’t ever fire. I don’t know if NSRunLoop is looking for the NSTimer class specifically or if it’s calling some private method that I need to override. If someone else has had any luck doing this, drop me a line.
The alternative is a category. The problem is that we need at least one ivar to store the original fire date. Normally, you can’t add ivars in a category but in this glorious post-10.6 age, we can, via associative references. It’s basically just a dictionary where you can store variables associated with an object. Sure, you could have implemented that yourself but the nice thing is that it handles memory management as well. When the object is dealloc’ed/collected, depending on the memory management you specify, it can also release/collect any associated objects.
So, NSTimer category it is. It supplies the method (among others)
+scheduledTimerWithAbsoluteFireDate:target:selector:userInfo: which creates and returns an autoreleased and scheduled timer that will fire on the given date regardless of any time fluctuations/lapses. If all of a sudden the fire date has passed (like if the machine was asleep during the fire date or if the system clock was set ahead), it will fire immediately. Note that calling
-setFireDate: won’t quite work in the sense that if the timer ever has to be reset from a time shift, it will be set to the date used when creating the timer, not the one that you set it to afterwards. This is an implementation limitation as I am using a category and can’t override methods (at least not without doing some method-swizzling which is not something I want to deal with). In cases where you need to change the fire date, I’d suggest just creating a new NSTimer.
I’ve also included a test harness where you can see it in action compared against a timer running in normal (non-absolute) mode. When run normally, both timers should fire at the same time but you can test things out by changing the system clock or putting your machine to sleep. You’ll see that the regular timer’s fire date will start to drift out while the “absolute” timer will stick with the time originally set.
I also went ahead and added methods to have NSTimer to use blocks instead of a target/selector combo. Note sure why this wasn’t in there already but I thought it’d be useful.
And that’s not all! Included is my NoodleGlue class. It’s a simple little class that just wraps a block (plus another block for cleanup, if you need it). It’s useful for cases where you want to set a target and selector for some object to use but don’t want to create a new class or method for it. Check out the source code for the NSTimer category to see how its used both with NSTimer (to implement the block API) as well as NSNotificationCenter. In the latter case, there is a blocks-based API but I had special memory management requirements that couldn’t be done with the existing API.
Needless to say, this extension only works on 10.6+. It’s in the latest version of NoodleKit and as a result I’ve made NoodleKit require 10.6 as well. Just build and run the TimerLab target. Fixes and suggestions welcome.
[Update 8:35pm EDT] I neglected to push the code to the github repository. Sorry about that. Everything should be there now.