Search code examples
ios6timernull

IOS Object respawn on timer after I set its ID to nil


DemoViewController is responsible for showing tutorial to the user. Contains animations and timer to repeat gesture demo while ignored by the user. Is instantiated form DataViewController. Is nil-ed, but later respawns on its internal timer. I need it to be completely gone so it is not created again when user returns to the first page.

dataViewController.h

#import "DemoViewController.h"
@property (strong,nonatomic) DemoViewController *demoController;

dataViewController.h

-(void) viewWillAppear:(BOOL)animated {
    // demoPageNumber is 0
    if ((self.demoController== nil) && ([_pageNumber isEqualToNumber:demoPageNumber])){
       self.demoController = [[DemoViewController alloc] initWithView:self.view];
    }
}

-(void) viewWillDisappear:(BOOL)animated{
    [self.demoController free]; // invalidate timer, nil all internal objects
    self.demoController=nil; // should free object
}

DemoViewController.m

-(void) free{
   [animationRespawnTimer invalidate];
   animationRespawnTimer=nil;
}

-(void) respawnDemoWithSelector:(SEL)selector{
    NSLog(@"Timer fired %@", self);
    [self resetTimer];
    animationRespawnTimer = [NSTimer scheduledTimerWithTimeInterval:10
                                                        target:self
                                                      selector:selector
                                                      userInfo:nil
                                                       repeats:NO];
}

-(void) showScrollDemo{


    NSLog(@"showScrollDemo fired");
    [self stopPreviousAnimations];
    scrollHandView.frame = CGRectMake(315.0, 700.0, 100, 100);
    scrollHandView.hidden=NO;
    scrollHandView.alpha=1;

    [UIView animateWithDuration:3.0
                          delay: 0.0
                        options: (UIViewAnimationOptionCurveEaseOut |
                                  UIViewAnimationOptionRepeat )
                     animations:^{

                         [UIView setAnimationRepeatCount:3];
                         scrollHandView.frame = CGRectMake(315.0, 300.0, 100, 100);

                     }
                     completion:^(BOOL finished){

                         [UIView animateWithDuration:1.0 delay:0 
                        options:UIViewAnimationOptionCurveEaseOut
                                      animations:^{
                                          scrollHandView.alpha=0;

                          }
                            completion:^(BOOL finished){
                            scrollHandView.hidden=YES;

                    [self respawnDemoWithSelector: @selector(showScrollDemo)];

                          }
                      ];

                 }
 ];

}

When the page is loaded and if this is the first page, demoController is instantiated, and on exit from the page nil-ed after clean-up (custom free method). According to my understanding this should delete the demoController object with all its contents including the timer. And debug area shows exactly that! Until when on the new page the demoController timer mystically respawns from nowhere with previous object ID.

17:59:14.041 viewWillAppear begin (self.demoController null)
18:00:05.346 viewWillAppear, <DemoViewController: 0x7580310> //demoController created
18:00:15.786 in the demoController method the "showScrollDemo" is fired
18:00:19.834 viewWillAppear end <DemoViewController: 0x7580310>

Page was loaded, demo performed fine. Now I'm flipping the page. viewWillDisappear event is fired.

18:01:17.966 viewWillDisappear begin, send "free" message to demoController 
18:01:17.966 "free" was performed from <DemoViewController: 0x7580310>
18:01:34.059 viewWillDisappear end (self.demoController null)

So, the "self.demoController" is null. Then the demoController respawns itself with previous ID

18:02:36.514 Timer fired <DemoViewController: 0x7580310>

Why? Timer can not respawn, it is set to repeats:NO.


Solution

  • I assume that it is the completion block of the animation that calls respawnDemoWithSelector and creates a new timer.

    According to this answer: https://stackoverflow.com/a/9676508/1187415, you can stop all running animations with

    [self.view.layer removeAllAnimations];
    

    Alternatively, you can add a boolean property done to the DemoViewController which is set to YES in the free method, and checked in the completion block of the animation:

    if (!self.done)
        [self respawnDemoWithSelector: @selector(showScrollDemo)];
    

    UPDATE: The animation blocks capture a strong reference to self, thus preventing the object from being deallocated. The standard solution to that "retain-cycle" problem (assuming that you use ARC) is to use a weak reference to self. That would look like this:

    __weak typeof(self) weakSelf = self;
    [UIView animateWithDuration:3.0
                          delay: 0.0
                        options: (UIViewAnimationOptionCurveEaseOut |
                                  UIViewAnimationOptionRepeat )
                     animations:^{
    
                         [UIView setAnimationRepeatCount:3];
                         weakSelf.scrollHandView.frame = CGRectMake(315.0, 300.0, 100, 100);
    
                     }
                     completion:^(BOOL finished){
    
                         [UIView animateWithDuration:1.0 delay:0
                                             options:UIViewAnimationOptionCurveEaseOut
                                          animations:^{
                                              weakSelf.scrollHandView.alpha=0;
    
                                          }
                                          completion:^(BOOL finished){
                                              weakSelf.scrollHandView.hidden=YES;
                                              [weakSelf respawnDemoWithSelector: @selector(showScrollDemo)];
    
                                          }
                          ];
    
                     }
     ];
    

    weakSelf does not hold a strong reference to the DemoViewController and is set to nil automatically if the object it points to is deallocated. In that case, all message sent to weakSelf inside the blocks do just nothing.