Monday, November 2, 2015

Swift NSTimer and Blocks (Closures)

Introduction


I'm always a bit surprised to discover that NSTimer doesn't have support, out of the box, for blocks or closures.  So I decided to put together this simple extension to NSTimer that supports swift closures.

The NSTimer Classic Way


NSTimer has been around for a long time.  It uses the objective-c runtime to invoke some code after a period of time. The invocation can be done by either an objective-c NSInvocation object or via selectors.  Let's look at a simple example of invoking a selector in swift using a NSTimer.

class SimpleTimerTest
{
    var timer : NSTimer?
    
    func someTask( timeout:NSTimeInterval )
    {
        self.timer = NSTimer.scheduledTimerWithTimeInterval( timeout,
            target:self,
            selector:"timerCallback:",
            userInfo:nil,
            repeats:false )
    }
    
    @objc func timerCallback( timer:NSTimer ) {
        FLLog.info( "*** Timer Fired ***" )
    }    
}

It's fairly easy to invoke the selector.  One important detail is that the method invoked must be defined with @objc.  This allows the objective-c runtime access to the method. However, there is no compile time check for this, if you forget the @objc modifier then you will get an "Unrecognized selector" runtime error when the selector is invoked.

There is also no spell checking for the selector in swift so if you mess up the selector string you will also get a runtime error. Objective-c has a @selector compiler directive which will validate the selector exists at compile time, but there is no equivalent to this in swift.

Another point to consider with this approach is that the target instance of the selector will be retained by the timer. This means for the above example that SimpleTimerTest will be retained until the timer is done with it.

These basic tools can be used to implement our own extension to NSTimer that allows a block to be invoked when the timer fires.

NSTimer Block


A simple NSTimer extension can be created that allows blocks or closures to be used.  Here is an implementation that does this:

public typealias FLTimerBlock = (timeinterval:NSTimeInterval) -> Void

extension NSTimer
{
    private class TimerBlockContainer {
        private(set) var timerBlock:FLTimerBlock
         
        init( timerBlock:FLTimerBlock ) {
            self.timerBlock = timerBlock;
        }
    }
    
    public class func scheduledTimerWithTimeInterval(timeInterval:NSTimeInterval,
                                                     repeats:Bool = false,
                                                     block:FLTimerBlock) -> NSTimer
    {
        return self.scheduledTimerWithTimeInterval(timeInterval,
            target: self,
            selector:"_executeBlockFromTimer:",
            userInfo:TimerBlockContainer(timerBlock:block),
            repeats:repeats)
    }
    
    @objc class func _executeBlockFromTimer( timer:NSTimer ) {
        if let timerBlockContainer = timer.userInfo as? TimerBlockContainer {
            timerBlockContainer.timerBlock(timeinterval:timer.timeInterval)
        }
    }
}

The timer extension does a couple of nice things:
  1. The @objc time method is contained in the extension
  2. No selector spelling mistakes outside the extension
  3. The NSTimer will retain the contained TimerBlockContainer as opposed to the class using the NSTimer.  If the block needs to retain references, it will do so according to the normal block rules.
It's really easy to put this in use, just call the timer method and give it a completion block:

        NSTimer.scheduledTimerWithTimeInterval(5.0, repeats: false)
        { (timeinterval) -> Void in
            FLLog.info( "*** Timer Fired ***" )
        }

One of the tricks of this implementation is that we are using the userInfo property of the NSTimer to store our block reference.  This allows it to exist for the duration of the lifetime of the NSTimer it's associated with.

NSTimer Block Weak


One of the advantages of using the NSTimer Block approach is that it doesn't force a retain.  So the following is possible:

class SimpleTimerTest
{
    var timer : NSTimer?
    var timeoutCount : Int = 0
    
    deinit
    {
        FLLog.info( "SimpleTimerTest deinit" )
        timer?.invalidate()
    }

    func someTask( timeout:NSTimeInterval )
    {
        self.timer = NSTimer.scheduledTimerWithTimeInterval(timeout, repeats:false)
        { [weak self] (timeinterval) -> Void in
            FLLog.info( "*** Timer Fired ***" )
            self?.timeoutCount += 1
        }
    }
}

Notice here that we mark the timer block as [weak self].  This means the block will not retain self, but then we are responsible for handling cases where the block might be called without self.  Also notice, that we go ahead and invalidate the timer when we deinit.  This will release the timer and the block won't be called after the instance goes away.  Because we are doing this in deinit, we can actually define the block as [unowned self]:

func someTask( timeout:NSTimeInterval )
{
   self.timer = NSTimer.scheduledTimerWithTimeInterval(timeout, repeats:false)
   { [unowned self] (timeinterval) -> Void in
      FLLog.info( "*** Timer Fired ***" )
      self.timeoutCount += 1
   }
}


We can safely do this because we guarantee that self won't be nil within the block as the block will never be called after deinit (because of the call to invalidate).

Conclusion


I hope you found this helpful and interesting.  If you see anything incorrect or if you have different thoughts please share.  If you are interested in swift or iOS development, be sure to follow this blog.

Thanks,
Tod Cunningham



2 comments:

  1. Thanks for the code examples. Still learning Swift, and this helps a lot.

    However, one thing might need changing: We're not supposed to use two-letter "name space" prefixes, at least for Obj-C names - those are reserved for Apple. We must use three letters, as Apple has stated a few years ago. Not sure if this still applies to Swift, though.

    ReplyDelete
    Replies
    1. Thanks for pointing out the 3 letter naming convention. I found a nice article on nshipster that discusses it from 2014 (http://nshipster.com/namespacing/). I haven't seen anything official on this from Apple for swift. This (http://stackoverflow.com/questions/24214863/swift-class-prefix-needed) reference from StackOverflow would indicate that the naming conventions wouldn't been need to resolve/avoid name collisions in swift.

      Delete