Search code examples
iphonecore-animationios4quartz-graphics

UIView infinite loop Animation to call a method after every repeat cycle


I am at my wits end trying to come up with a design pattern for this paradigm. I haven't had much luck on this site but at this stage I'll try anything.

I am trying to implement a radar type animation and hence I am rotating a view 360 degrees to represent the radius rotating around the circle. I have placed points around this circle and am able to calculate the angle from the center of the circle using standard trig.

As the radius sweeps around the circle, if it intersects with a point (eg angle of the point equals the angle of the sweeping radius + a tolerance) the point flashes on.

Now, I have approached this paradigm in a number of ways but have not achieved an ideal solution. Here is what I have tried.

FIRST: iOS4 animation blocks. I would rotated the radius 10 degrees at a time with a duration of .1sec and in the completion method, check for intersections with points. The problem here is that if you set the option to repeat, then the completion method doesn't get called. The only time the completion method gets called is when the entire animation terminates, not after every repeat cycle, so this solution doesn't work.

SECOND: Tried explicit animation using CABasicAnimation. Same approach as above, rotating 10 degrees at every .1 seconds and setting the delegate to self and implementing the animationDidFinish method. In the animationDidFinish method, I checked for intersections with point. I set the animation to cumulative and repeatCound to Huge Value. The raduis rotates but once again, the animationDidFinish doesn't get called unless I set the repeatCount to a small number and then it only gets called at the end of the repeatCount, not after every repeat cycle.

THIRD: Using NSTimer and this approach actually works but the animation can be jerky depending on what is going on around the screen. I use a timer tick of .1 sec and initiate a single animation of 10 degrees as well as a intersection check at every tick. The problem with this approach is that it is susceptible to being stalled when starting each animation due to the processor doing other animations in the background. Note that i didn't have this problem with the other two approaches!

FOURTH: I tried using a combination of the two. CABasicAnimation on the radius and NSTimer on the Points. This issue here is that they can get out of sync and happens quite easily if the iDevice goes to sleep and then resumes.

FIFTH: Using the iOS3.0 style animation block and rotating 10 degrees with a duration of .1sec. Setting the delegate and the animationDidStopSelector with the animationDidStop method calling the animation method again as well as checking of intersection with points. This works too but much like the third solution, it is jerky when scrolling and other animations are happening. This is most likely caused by the stop start nature of the animation.

Basically, is there a way to animation a view infinitely but make it call a method after every repeat cycle? Or is there a way to make the third solution work more smoothly?

PLEASE HELP, I HAVE RUN OUT OF DESIGN PATTERNS.


Solution

  • OK, I have come a long way since I posted this question.

    The accepted design pattern is to actually generate a clock tick (commonly using CADisplayLink) that runs at either 30 frames per second or 60 depending on how smooth and fast you want the animation to be. At every frame callback, you modify the transform on the view/layer and use a transaction to display it.

    Some key notes: Build a method to test for intersecting line and point:

    -(BOOL)intersectWithAngle:(CGFloat)rad {        
        return (rad <= angle && rad >= angle - angleIncrement);
    }
    

    You must use transactions when attempting to modify a property that is animatable to prevent the layer/view from animating the new value itself. (You don't want to animate it twice)

    [CATransaction begin];
    [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
    
    self.transform = rotationTransform;
    
    [CATransaction commit];
    

    No need to keep creating new transforms every frame, this is just wasteful and consumes resources. Best create an instance variable / property called rotationTransform and modify that every frame and re-apply it to the layer/view.

    -(void)displayFrame {   
        rotationTransform.a = cos(angle);
        rotationTransform.b = sin(angle);
        rotationTransform.c = -sin(angle);
        rotationTransform.d = cos(angle);
    
        [CATransaction begin];
        [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
    
        self.transform = rotationTransform;
    
        [CATransaction commit];
    }
    

    Next, create a method to actually set up the angle, use an instance variable / property to store the angle and define your angle incrementation (I use 3 degrees), also predefine 2-pi so that you are not always recalculating it.

    -(void)processFrame {
        angle += angleIncrement;
        if (angle >= M_2PI) {
            angle -= M_2PI;
        }
    
        if ([self intersectWithAngle:point.angle]) {
            [point animate];
        } 
    }
    

    Finally, build your method that is called by CADisplayLink which sets up the frame and displays it while maintaining frame sync.

    -(void)displayLinkDidTick {
        // get the time now
        NSTimeInterval timeNow = [NSDate timeIntervalSinceReferenceDate];
    
        if (timeOfLastDraw == 0) {
            numberOfTicks = 1;
        } else {
            NSTimeInterval timeSinceLastDraw = timeNow - timeOfLastDraw;
            NSTimeInterval desiredTimeInterval = kFrameDuration;
    
            numberOfTicks = (NSUInteger)(timeSinceLastDraw / desiredTimeInterval);      
        }
    
        if (numberOfTicks > 4) {
            numberOfTicks = 4;
            timeOfLastDraw = timeNow;
        } else {
            timeOfLastDraw += numberOfTicks * kFrameDuration;
        }
    
    
        while(numberOfTicks--) {
    
            [self processFrame];
            if (spriteState == SpriteStateIdle)
                break;
            }
    
        [self displayFrame];
    
    }
    

    This code extract has been heavily modified for this post, in actually fact I do the animation of the scan line and the blipping of the point in the same CADisplayLink instance.