Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weak Reference to NSTimer Target To Prevent Retain Cycle

The proposed code:

__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

has the effect that (i) a weak reference is made to self; (ii) that weak reference is read in order to provide a pointer to NSTimer. It won't have the effect of creating an NSTimer with a weak reference. The only difference between that code and using a __strong reference is that if self is deallocated in between the two lines given then you'll pass nil to the timer.

The best thing you can do is create a proxy object. Something like:

[...]
@implementation BTWeakTimerTarget
{
    __weak target;
    SEL selector;
}

[...]

- (void)timerDidFire:(NSTimer *)timer
{
    if(target)
    {
        [target performSelector:selector withObject:timer];
    }
    else
    {
        [timer invalidate];
    }
}
@end

Then you'd do something like:

BTWeakTimerTarget *target = [[BTWeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];

Or even add a class method to BTWeakTimerTarget of the form +scheduledTimerWithTimeInterval:target:selector:... to create a neater form of that code. You'll probably want to expose the real NSTimer so that you can invalidate it, otherwise the rules established will be:

  1. the real target isn't retained by the timer;
  2. the timer will fire once after the real target has begun (and probably completed) deallocation, but that firing will be ignored and the timer invalidated then.

iOS 10 and macOS 10.12 "Sierra" introduced a new method, +scheduledTimerWithTimeInterval:repeats:block:, so you could capture self weakly simply as:

__weak MyClass* weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) {
    MyClass* _Nullable strongSelf = weakSelf;
    [strongSelf doSomething];
}];

Equivalence in Swift 3:

_timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.doSomething()
}

If you still need to target iOS 9 or below (which you should at this moment), this method cannot be used, so you would still need to use code in the other answers.


If you are not that concerned about the millisecond accuracy of the timer events, you could use dispatch_after & __weak instead of NSTimer to do this. Here's the code pattern:

- (void) doSomethingRepeatedly
{
    // Do it once
    NSLog(@"doing something …");

    // Repeat it in 2.0 seconds
    __weak typeof(self) weakSelf = self;
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [weakSelf doSomethingRepeatedly];
    });
}

No NSTimer @property, no invalidate/runloop stuff and no proxy object, just a simple clean method.

The downside of this approach is that (unlike NSTimer) the execution time of the block (containing [weakSelf doSomethingRepeatedly];) will impact scheduling of the events.


Swift 3

App target < iOS 10:

Custom WeakTimer (GitHubGist) implementation:

final class WeakTimer {

    fileprivate weak var timer: Timer?
    fileprivate weak var target: AnyObject?
    fileprivate let action: (Timer) -> Void

    fileprivate init(timeInterval: TimeInterval,
         target: AnyObject,
         repeats: Bool,
         action: @escaping (Timer) -> Void) {
        self.target = target
        self.action = action
        self.timer = Timer.scheduledTimer(timeInterval: timeInterval,
                                          target: self,
                                          selector: #selector(fire),
                                          userInfo: nil,
                                          repeats: repeats)
    }

    class func scheduledTimer(timeInterval: TimeInterval,
                              target: AnyObject,
                              repeats: Bool,
                              action: @escaping (Timer) -> Void) -> Timer {
        return WeakTimer(timeInterval: timeInterval,
                         target: target,
                         repeats: repeats,
                         action: action).timer!
    }

    @objc fileprivate func fire(timer: Timer) {
        if target != nil {
            action(timer)
        } else {
            timer.invalidate()
        }
    }
}

Usage:

let timer = WeakTimer.scheduledTimer(timeInterval: 2,
                                     target: self,
                                     repeats: true) { [weak self] timer in
                                         // Place your action code here.
}

timer is instance of standard class Timer, so you can use all available methods (e.g. invalidate, fire, isValid, fireDate and etc).
timer instance will be deallocated when self is deallocated or when timer's job is done (e.g. repeats == false).

App target >= iOS 10:
Standard Timer implementation:

open class func scheduledTimer(withTimeInterval interval: TimeInterval, 
                               repeats: Bool, 
                               block: @escaping (Timer) -> Swift.Void) -> Timer

Usage:

let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
    // Place your action code here.
}