Search code examples
iosobjective-canimationrecursioncompletion-block

Animation completion block running too soon


I am trying to move a plane along a path as the user is defining it, similar to the controls in Flight Control. I am able to draw the path, albeit inefficiently, but I cannot get the plane to animate along the path smoothly.

I originally tried to move the plane along the path by using the implicit animations provided by changing the position, but because it does not allow me to alter the speed of the animation, the plane behaves badly.

I am now trying to use an animation block recursively, but the completion block is being called way too soon, resulting in the plane just following the path being drawn.

This is the setup code:

- (CALayer *)plane{
    if(!_plane){
        _plane = [CALayer layer];
        UIImage *planeImage = [UIImage imageNamed:@"airplane.png"];
        _plane.bounds = CGRectMake(20.0, 20.0, planeImage.size.width, planeImage.size.height);
        _plane.contents = (id)(planeImage.CGImage);
        [self.layer addSublayer:_plane];
    }
    return _plane;
}
- (void)touchesEnded:(NSSet *)touches
           withEvent:(UIEvent *)event{
    if(isDrawingPath){
        [self.trackPath addLineToPoint:[[touches anyObject] locationInView:self]];
        NSLog(@"%@", self.trackPath);

        UITouch *touch = [touches anyObject];
        CGPoint toPoint = [touch locationInView:self];
        //now, save each point in order to make the path
        [self.points addObject:[NSValue valueWithCGPoint:toPoint]];
        [self setNeedsDisplay];
        [self goToPointWithIndex:0];
    }
    isDrawingPath = NO;
}

This implementation works, but badly. The plane follows the path, but it is choppy:

- (void)goToPointWithIndex:(NSNumber *)indexer{
    int toIndex = [indexer intValue];

    if(toIndex < self.points.count){
        //extract the value from array
        CGPoint toPoint = [(NSValue *)[self.points objectAtIndex:toIndex] CGPointValue];
        CGPoint pos = self.plane.position;
        float delay = PLANE_SPEED * (sqrt( pow(toPoint.x - pos.x, 2) + pow(toPoint.y - pos.y, 2)));
        self.plane.position = toPoint;

        // Allows animation to continue running
        if(toIndex < self.points.count - 1){
            toIndex++;
        }

        //float delay = 0.2;
        NSLog(@"%f", delay);
        //repeat the method with a new index
        //this method will stop repeating as soon as this "if" gets FALSE
        NSLog(@"index: %d, x: %f, y: %f", toIndex, toPoint.x, toPoint.y);
        [self performSelector:@selector(goToPointWithIndex:) withObject:[NSNumber numberWithInt:toIndex] afterDelay:delay];
    }
}

This is what I am trying to do with blocks. It just skips to the end of the path drawn instead of follow the entire thing.

- (void)goToPointWithIndex:(int)toIndex{
    if(self.resetPath) return;
    //extract the value from array
    if(toIndex < self.points.count) {
        CGPoint toPoint = [(NSValue *)[self.points objectAtIndex:toIndex] CGPointValue];
        NSLog(@"index: %d, x: %f, y: %f", toIndex, toPoint.x, toPoint.y);
        CGPoint pos = self.plane.position;
        //float delay = PLANE_SPEED * (sqrt( pow(toPoint.x - pos.x, 2) + pow(toPoint.y - pos.y, 2)));

        // Allows animation to continue running
        if(toIndex < self.points.count - 1){
            toIndex++;
        }

        [UIView animateWithDuration:0.2
                     animations:^{
                         self.plane.position = toPoint;
                     }
                completion:^(BOOL finished) {
                    if(finished == YES) {NSLog(@"Complete");[self goToPointWithIndex:toIndex];}
        }];
    }
}

I have no idea where I'm going wrong. I'm new to objective-c and blocks. Is the completion block supposed to run right after the animation begins? It doesn't make sense to me, but its the only explanation I can find.


EDIT: I ended up making the plane a UIView so I could still use block animations. The CALayer transactions did not like my recursion very much at all.


Solution

  • Since here self.plane is a custom CALayer (as opposed to the layer backing a UIView), it will not respect the parameters of UIView animateWithDuration:

    From the docs:

    Custom layer objects ignore view-based animation block parameters and use the default Core Animation parameters instead.

    Instead use a CABasicAnimation and CALayer's addAnimation:forKey: method (and probably in this case a CAAnimationGroup). How to animate CALayers like this is described in the docs under "Animating Layer Content."