Search code examples
iosuikitcakeyframeanimation

UIView keyframe animation timing not as expected


I have some animations (testable in a playground):

import UIKit
import XCPlayground
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution


let view = UIView()
view.frame = .init(x: 0, y: 0, width: 150, height: 150)
view.backgroundColor = .orange
let hand = UIView()
view.addSubview(hand)
hand.frame = .init(x: 0, y: 0, width: 10, height: 10)
hand.center = view.center
hand.backgroundColor = .green
PlaygroundPage.current.liveView = view

let fadeDuration: TimeInterval = 0.4
let translationDuration: TimeInterval = 1
let resetDuration: TimeInterval = 0.25
let duration: TimeInterval =
    7 * fadeDuration +
    4 * translationDuration +
    3 * resetDuration
var currentTime: TimeInterval  = 0.0

func addKey(_ animations: @escaping () -> Void, animationTime: TimeInterval) {
    UIView.addKeyframe(withRelativeStartTime: currentTime / duration, relativeDuration: animationTime / duration, animations: animations)
    currentTime += animationTime
}

func fadeIn() {
    addKey({
        hand.alpha = 1
    }, animationTime: fadeDuration)
}

func fadeOut() {
    addKey({
        hand.alpha = 0
    }, animationTime: fadeDuration)
}

func translate(_ direction: (CGFloat) -> CGFloat) {
    let x = direction(50)
    addKey({
        hand.transform = .init(translationX: x, y: 0)
    }, animationTime: translationDuration)
}

func reset() {
    addKey({
        hand.transform = .identity
    }, animationTime: 0)
    currentTime += resetDuration
}

UIView.animateKeyframes(withDuration: duration, delay: 0, options:  .calculationModeLinear) {
    translate(-)
    fadeOut()
    reset()
    fadeIn()
    translate(-)
    fadeOut()
    reset()
    fadeIn()
    translate(+)
    fadeOut()
    reset()
    fadeIn()
    translate(+)
    fadeOut()
} completion: { _ in
    PlaygroundPage.current.finishExecution()
}

I expect every single translate() animation to run at the same speed, but for some reason the first and last ones are especially slow, despite using .calculationModeLinear

How to make it so that translationDuration / duration is constant time ?


Solution

  • calculationModeLinear is different from curveLinear. It sounds like you want the latter. You are using curveEaseInOut by default so of course the movement slows near the overall start and finish.

    Some hanky panky is needed to overcome Swift’s strict typing. Sample code:

    let animationOptions: UIView.AnimationOptions = .curveLinear
    let keyframeAnimationOptions = UIView.KeyframeAnimationOptions(rawValue: animationOptions.rawValue)
    UIView.animateKeyframes(withDuration: duration, delay: 0.1, options:  keyframeAnimationOptions) {