How can I animate the movement of a view or image along a curved path?

Solution 1:

To expand upon what Nikolai said, the best way to handle this is to use Core Animation to animate the motion of the image or view along a Bezier path. This is accomplished using a CAKeyframeAnimation. For example, I've used the following code to animate an image of a view into an icon to indicate saving (as can be seen in the video for this application):

First of all import QuartzCore header file #import <QuartzCore/QuartzCore.h>

UIImageView *imageViewForAnimation = [[UIImageView alloc] initWithImage:imageToAnimate];
imageViewForAnimation.alpha = 1.0f;
CGRect imageFrame = imageViewForAnimation.frame;
//Your image frame.origin from where the animation need to get start
CGPoint viewOrigin = imageViewForAnimation.frame.origin;
viewOrigin.y = viewOrigin.y + imageFrame.size.height / 2.0f;
viewOrigin.x = viewOrigin.x + imageFrame.size.width / 2.0f;

imageViewForAnimation.frame = imageFrame;
imageViewForAnimation.layer.position = viewOrigin;
[self.view addSubview:imageViewForAnimation];

// Set up fade out effect
CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
[fadeOutAnimation setToValue:[NSNumber numberWithFloat:0.3]];
fadeOutAnimation.fillMode = kCAFillModeForwards;
fadeOutAnimation.removedOnCompletion = NO;

// Set up scaling
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:@"bounds.size"];
[resizeAnimation setToValue:[NSValue valueWithCGSize:CGSizeMake(40.0f, imageFrame.size.height * (40.0f / imageFrame.size.width))]];
resizeAnimation.fillMode = kCAFillModeForwards;
resizeAnimation.removedOnCompletion = NO;

// Set up path movement
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
//Setting Endpoint of the animation
CGPoint endPoint = CGPointMake(480.0f - 30.0f, 40.0f);
//to end animation in last tab use 
//CGPoint endPoint = CGPointMake( 320-40.0f, 480.0f);
CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, viewOrigin.x, viewOrigin.y);
CGPathAddCurveToPoint(curvedPath, NULL, endPoint.x, viewOrigin.y, endPoint.x, viewOrigin.y, endPoint.x, endPoint.y);
pathAnimation.path = curvedPath;
CGPathRelease(curvedPath);

CAAnimationGroup *group = [CAAnimationGroup animation]; 
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[group setAnimations:[NSArray arrayWithObjects:fadeOutAnimation, pathAnimation, resizeAnimation, nil]];
group.duration = 0.7f;
group.delegate = self;
[group setValue:imageViewForAnimation forKey:@"imageViewBeingAnimated"];

[imageViewForAnimation.layer addAnimation:group forKey:@"savingAnimation"];

[imageViewForAnimation release];

Solution 2:

The way to animate along CGPath using UIView.animateKeyframes (Swift 4)

private func animateNew() {

   let alphaFrom: CGFloat = 1
   let alphaTo: CGFloat = 0.3
   let sizeFrom = CGSize(width: 40, height: 20)
   let sizeTo = CGSize(width: 80, height: 60)
   let originFrom = CGPoint(x: 40, y: 40)
   let originTo = CGPoint(x: 240, y: 480)

   let deltaWidth = sizeTo.width - sizeFrom.width
   let deltaHeight = sizeTo.height - sizeFrom.height
   let deltaAlpha = alphaTo - alphaFrom

   // Setting default values
   imageViewNew.alpha = alphaFrom
   imageViewNew.frame = CGRect(origin: originFrom, size: sizeFrom)

   // CGPath setup for calculating points on curve.
   let curvedPath = CGMutablePath()
   curvedPath.move(to: originFrom)
   curvedPath.addQuadCurve(to: originTo, control: CGPoint(x: originFrom.x,  y: originTo.y))
   let path = Math.BezierPath(cgPath: curvedPath, approximationIterations: 10)

   // Calculating timing parameters
   let duration: TimeInterval = 0.7
   let numberOfKeyFrames = 16
   let curvePoints = Math.Easing.timing(numberOfSteps: numberOfKeyFrames, .easeOutQuad)

   UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.calculationModeCubic], animations: {
      // Iterating curve points and adding key frames
      for point in curvePoints {
         let origin = path.point(atPercentOfLength: point.end)
         let size = CGSize(width: sizeFrom.width + deltaWidth * point.end,
                           height: sizeFrom.height + deltaHeight * point.end)
         let alpha = alphaFrom + deltaAlpha * point.end
         UIView.addKeyframe(withRelativeStartTime: TimeInterval(point.start), relativeDuration: TimeInterval(point.duration)) {
            self.imageViewNew.frame = CGRect(origin: origin, size: size)
            self.imageViewNew.alpha = alpha
         }
      }
   }, completion: nil)
}

File: Math.Easing.swift

// Inspired by: RBBAnimation/RBBEasingFunction.m: https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBEasingFunction.m
extension Math { public struct Easing { } }

extension Math.Easing {

   public enum Algorithm: Int {
      case linear, easeInQuad, easeOutQuad, easeInOutQuad
   }

   @inline(__always)
   public static func linear(_ t: CGFloat) -> CGFloat {
      return t
   }

   @inline(__always)
   public static func easeInQuad(_ t: CGFloat) -> CGFloat  {
      return t * t
   }

   @inline(__always)
   public static func easeOutQuad(_ t: CGFloat) -> CGFloat  {
      return t * (2 - t)
   }

   @inline(__always)
   public static func easeInOutQuad(_ t: CGFloat) -> CGFloat  {
      if t < 0.5 {
         return 2 * t * t
      } else {
         return -1 + (4 - 2 * t) * t
      }
   }
}

extension Math.Easing {

   public struct Timing {

      public let start: CGFloat
      public let end: CGFloat
      public let duration: CGFloat

      init(start: CGFloat, end: CGFloat) {
         self.start = start
         self.end = end
         self.duration = end - start
      }

      public func multiplying(by: CGFloat) -> Timing {
         return Timing(start: start * by, end: end * by)
      }
   }

   public static func process(_ t: CGFloat, _ algorithm: Algorithm) -> CGFloat {
      switch algorithm {
      case .linear:
         return linear(t)
      case .easeInQuad:
         return easeInQuad(t)
      case .easeOutQuad:
         return easeOutQuad(t)
      case .easeInOutQuad:
         return easeInOutQuad(t)
      }
   }

   public static func timing(numberOfSteps: Int, _ algorithm: Algorithm) -> [Timing] {
      var result: [Timing] = []
      let linearStepSize = 1 / CGFloat(numberOfSteps)
      for step in (0 ..< numberOfSteps).reversed() {
         let linearValue = CGFloat(step) * linearStepSize
         let processedValue = process(linearValue, algorithm) // Always in range 0 ... 1
         let lastValue = result.last?.start ?? 1
         result.append(Timing(start: processedValue, end: lastValue))
      }
      result = result.reversed()
      return result
   }
}

File: Math.BezierPath.swift. Look on this SO answer: https://stackoverflow.com/a/50782971/1418981

enter image description here

Solution 3:

You can animate a UIView's center property using a CAKeyframeAnimation. See the CoreAnimation programming guide.