iosswiftuiviewuiviewpropertyanimator

UIViewPropertyAnimator not respecting duration and delay parameters


I have a very strange issue in my app. I'm using UIViewPropertyAnimator to animate changing images inside UIImageView.

Sounds like trivial task but for some reaso my view ends up changing the image instantly so I end up with images flashing at light speed instead of given duration and delay parameters.

Here's the code:

private lazy var images: [UIImage?] = [
    UIImage(named: "widgethint"),
    UIImage(named: "shortcuthint"),
    UIImage(named: "spotlighthint")
]
private var imageIndex = 0
private var animator: UIViewPropertyAnimator?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    animator = repeatingAnimator()
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    animator?.stopAnimation(true)
}

private func repeatingAnimator() -> UIViewPropertyAnimator {
    return .runningPropertyAnimator(withDuration: 2, delay: 6, options: [], animations: {
        self.imageView.image = self.images[self.imageIndex]
    }, completion: { pos in
        if self.imageIndex >= self.images.count - 1 {
            self.imageIndex = 0
        } else {
            self.imageIndex += 1
        }
        self.animator = self.repeatingAnimator()
    })
}

So, according to the code animation should take 2 seconds to complete and start after 6 seconds delay, but it starts immediately and takes milliseconds to complete so I end up with horrible slideshow. Why could it be happening?

I also tried using the UIViewPropertyAnimator(duration: 2, curve: .linear) and calling repeatingAnimator().startAnimation(afterDelay: 6) but the result is the same.


Solution

  • Okay, I've figured it out, but it's still a bit annoying that such simple task cannot be done using the "Modern" animation API.

    So, animating images inside UIImageView is apparently not supported by UIViewPropertyAnimator, during debugging I tried animating view's background color and it was working as expected. So I had to use Timer instead and old UIView.transition(with:) method

    Here's the working code:

    private let animationDelay: TimeInterval = 2.4
    
    private var animationTimer: Timer!
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        setAnimation(true)
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        setAnimation(false)
    }
    
    private func setAnimation(_ enabled: Bool) {
        guard enabled else {
            animationTimer?.invalidate()
            animationTimer = nil
            return
        }
        animationTimer = Timer.scheduledTimer(withTimeInterval: animationDelay, repeats: true) { _ in
            UIView.transition(with: self.imageView, duration: 0.5, options: [.curveLinear, .transitionCrossDissolve], animations: {
                self.imageView.image = self.images[self.imageIndex]
            }, completion: { succ in
                guard succ else {
                    return
                }
                if self.imageIndex >= self.images.count - 1 {
                    self.imageIndex = 0
                } else {
                    self.imageIndex += 1
                }
            })
        }
    }
    

    Hopefully it will save someone some headaches in the future