0

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.

Janis Jakaitis
  • 327
  • 1
  • 8

1 Answers1

1

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.

Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Yes, the animation is in the method being called, but if the object is nil-ed how can I still access its method? I instantiate DemoViewController, call its method "showScrollDemo". Then I nil the object, and call the method. It should return nil and do nothing, right? – Janis Jakaitis Jun 30 '13 at 17:08
  • 1
    @JanisJakaitis: But the animation block captures a reference to `self`, therefore it is not deallocated, even if you call `self.demoController=nil`. At least that is what I assume. Did you try my suggestion? – Martin R Jun 30 '13 at 17:10
  • [self.view.layer removeAllAnimations]; is not changing anything. "Done" is a workaround, I'd like to find a way to kill animation and timer inside the "free" method. Restore the system to the point what it was before, when demoController was not yet instantiated. – Janis Jakaitis Jun 30 '13 at 17:23
  • 1
    @JanisJakaitis: A weak reference to self could be the solution. Will update answer later, currently writing on the phone. – Martin R Jun 30 '13 at 17:30
  • @JanisJakaitis: I have updated the answer, I hope that helps now. – Martin R Jun 30 '13 at 17:58