18

I have a UIImageView placed in UIScrollView, Basicly this UIImageView holds very big map, and created animation on a predefined path with "arrows" pointed navigation direction.

But, whenever uiscrollevents occurs, I think MainLoop freezes and NSTimer being not fired, and animation stopped.

Are there any existing property, which solves this problem, on UIScrollView, CAKeyFrameAnimation or NSTimer?

//viewDidLoad
   self.myTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(drawLines:) userInfo:nil repeats:YES];

- (void)drawLines:(NSTimer *)timer {

   CALayer *arrow = [CALayer layer];
   arrow.bounds = CGRectMake(0, 0, 5, 5);
   arrow.position = CGPointMake(line.x1, line.y1);
   arrow.contents = (id)([UIImage imageNamed:@"arrow.png"].CGImage);

   [self.contentView.layer addSublayer:arrow];

   CAKeyframeAnimation* animation = [CAKeyframeAnimation animation];
   animation.path = path;
   animation.duration = 1.0;
   animation.rotationMode = kCAAnimationRotateAuto; // object auto rotates to follow the path
   animation.repeatCount = 1;
   animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
   animation.fillMode = kCAFillModeForwards;

   [arrow addAnimation:animation forKey:@"position"];
}
ubaltaci
  • 1,016
  • 1
  • 16
  • 27
  • possible duplicate of [UIScrollView pauses NSTimer while scrolling](http://stackoverflow.com/questions/7059366/uiscrollview-pauses-nstimer-while-scrolling) – rob mayoff Feb 01 '12 at 04:24

2 Answers2

63

iOS Applications run on an NSRunLoop. Each NSRunLoop has different modes of execution for different tasks. For example, the default nstimer is scheduled to run under the NSDefaultRunMode on the NSRunLoop. What this means however is that certain UIEvents, scrollviewing being one, will interrupt the timer, and place it on a queue to be run as soon as the event stops updating. In your case, in order to get the timer to not be interrupted, you need to schedule it for a different mode, namely NSRunLoopCommonModes, like so:

  self.myTimer =  [NSTimer scheduledTimerWithTimeInterval:280
                                                                 target:self
                                                               selector:@selector(doStuff)
                                                               userInfo:nil
                                                                repeats:NO];
  [[NSRunLoop currentRunLoop] addTimer:self.myTimer forMode:NSRunLoopCommonModes]; 

This mode will allow your timer to not be interrupted by scrolling. You can find more about this info here: https://developer.apple.com/documentation/foundation/nsrunloop At the bottom you will see the definitions of the modes you can choose from. Also, legend has it, you can write your own custom modes, but few have ever lived to tell the tale im afraid.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Greg Price
  • 2,556
  • 1
  • 24
  • 33
  • 4
    Hey @GregPrice. Thanks for the explanation, even 3 years after. However, the doc says `scheduledTimerWithTimeInterval:...` creates a timer and *schedules it on the current run loop in the default mode*. So why do you add it twice in the default RunLoop, not just once in `NSRunLoopCommonModes` mode? – Martin May 20 '15 at 12:00
  • 1
    `UIScrollView` runs the run loop in `UITrackingRunLoopMode`, which is different than `NSDefaultRunLoopMode`. It's not enough to schedule the timer for the default mode if you also want it to run in other modes. – rob mayoff Mar 09 '16 at 22:38
  • 5
    For Swift 3: `RunLoop.current.add(self.myTimer, forMode: .commonModes)` – Steve Cotner Dec 20 '16 at 22:36
1

One more thing (c)

  1. use timerWithTimeInterval method in order to avoid adding timer to runloop in DefaultMode
  2. use mainRunLoop

So:

self.myTimer = [NSTimer timerWithTimeInterval:280 target:self selector:@selector(doStuff) userInfo:nil repeats:NO]; [[NSRunLoop mainRunLoop] addTimer:self.myTimer forMode:NSRunLoopCommonModes];

Andrey Seredkin
  • 671
  • 5
  • 7