1

I wrote the following little demo that rotates a UIView 360° by rotating it 90° at a time. That means the animation has 4 steps

I wanted it to ease in on the first animation step, go at a steady pace for the middle 2 steps, and then use ease out timing for the last step so it coasts to a stop. The code is below. Here is the animation timing it uses:

Animating to  90°, options = curveEaseIn
Animating to 180°, options = curveLinear
Animating to 270°, options = curveLinear
Animating to   0°, options = curveEaseOut

Each step takes 1/2 second, for a total duration of 2 seconds. However, since the first and last steps take 1/2 second but start/end at a slower pace, the "full speed" part of those animation steps is noticeably faster than the middle 2 animation steps that use linear timing. The animation is not smooth as a result.

Is there an easy way to adjust the step timing so each of the steps in the animation runs at the same pace as the beginning/end step that has ease in/ease out timing?

I guess I could instead create a keyframe animation where the entire animation uses ease-in/ease-out timing, and the intermediate steps inside the animation use linear timing. It seems like there should be an easy way to get that out of step-wise animation however.

class ViewController: UIViewController {
    
    var rotation: CGFloat = 0
    @IBOutlet weak var rotateableView: RotatableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func handleRotateButton(_ sender: UIButton? = nil) {
        let duration = 0.5
        var optionsValue: (options: UIView.AnimationOptions, optionsName: String) = (.curveLinear, "curveLinear")
        self.rotation = fmod(self.rotation + 90, 360)
        sender?.isEnabled = false
        if self.rotation == 0 {
            optionsValue = (.curveEaseOut, "curveEaseOut") // Ending the animatino, so ease out.
        } else if self.rotation == 90 {
            optionsValue = (.curveEaseIn, "curveEaseIn") // Beginning the animation, so ease in
        }
        let rotation = String(format:"%3.0f", self.rotation)
        print("Animating to \(rotation)°, options = \(optionsValue.optionsName)")
        UIView.animate(withDuration: duration, delay: 0, options: optionsValue.options) {
            let angle = self.rotation / 180.0 * CGFloat.pi
            self.rotateableView.transform = CGAffineTransform.init(rotationAngle: angle)
        } completion: { finished in
            if self.rotation != 0 {
                self.handleRotateButton(sender)
            } else {
                self.rotation = 0
                sender?.isEnabled = true
            }
        }
    }
}
Duncan C
  • 128,072
  • 22
  • 173
  • 272

1 Answers1

0

Ok, I couldn't figure out how to adjust the timing of a series of step-wise animations to get them to ease into the first step, use linear timing for the intermediate steps, and use ease out on the final steps.

However, keyframe animation apparently defaults to ease-in, ease-out timing for the whole animation. That makes creating ease-in, ease-out timing for a whole sequence of steps very easy. It looks like this:

@IBAction func handleRotateButton(_ sender: UIButton? = nil) {

    UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: []) {
        for index in 1...4 {
            let startTime = Double(index-1) / 4
            UIView.addKeyframe(withRelativeStartTime: startTime,
                               relativeDuration: 0.25,
                               animations: {
                                let angle: CGFloat = CGFloat(index) / 2 * CGFloat.pi
                                self.rotateableView.transform = CGAffineTransform.init(rotationAngle: angle)
                               }
            )
        }
    } completion: { completed in
        self.rotation = 0
        sender?.isEnabled = true
    }
}

Further, apparently you can pass in the same animation curve flags into UIView.animateKeyframes() that you can use for the options to animate(withDuration:delay:options:animations:completion:), if you simply create UIView.KeyframeAnimationOptions using the raw value from UIView.AnimationOptions. You can do that with an extension like this:

extension UIView.KeyframeAnimationOptions {
    init(animationOptions: UIView.AnimationOptions) {
        self.init(rawValue: animationOptions.rawValue)
    }
}

Then you can use code like this to create KeyframeAnimationOptions from AnimationOptions:

let keyframeOptions = UIView.KeyframeAnimationOptions(animationOptions: .curveLinear)
Duncan C
  • 128,072
  • 22
  • 173
  • 272