repeatCount in UIViewPropertyAnimator

I have a func to flash a view a number of times and execute closure at the end.

The following code with UIView.setAnimationRepeatCount works OK: animation occurs the requested number of times and afterEnd closure executes at the end.

    func flashIt(repeated: Int, cycleTime: Double, delayed: Double = 5.0, afterEnd: (() -> Void)? = nil) { 

        if repeated < 0 { return }
        let initAlpha = self.alpha
        UIView.animate(withDuration: cycleTime, 
            delay: delayed,
            options:[.allowUserInteraction, .curveEaseInOut, .autoreverse, .repeat],
            animations: {
                UIView.setAnimationRepeatCount(Float(repeated)) 
                self.alpha = 0.1    // Not 0.0, to allow user interaction
        },
            completion: { (done: Bool) in
                self.alpha = initAlpha
                afterEnd?() 
        } )
    }

To address UIView.setAnimationRepeatCount deprecation, I now try this (inspired by https://stackoverflow.com/questions/47496584/uiviewpropertyanimator-reverse-animation)

    func flashIt(repeated: Int, cycleTime: Double, delayed: Double = 5.0, afterEnd: (() -> Void)? = nil) { 

        if repeated < 0 { return }
        let initAlpha = self.alpha
        
        let animator = UIViewPropertyAnimator(duration: cycleTime, curve: .linear) {
            self.alpha = 0.1
        }
        
        animator.addCompletion { _ in
            let reverseAnimator = UIViewPropertyAnimator(duration: cycleTime, curve: .linear) {
                self.alpha = initAlpha
            }
            
            reverseAnimator.addCompletion { [self] _ in
                flashIt(repeated: repeated-1, cycleTime: cycleTime, delayed: 0) // without delay here
            }
            reverseAnimator.startAnimation()
            if repeated <= 1 {         // 1 and not 0, otherwise an extra loop…
                afterEnd?()            // Not called
                return 
            }
       }
        animator.startAnimation(afterDelay: delayed)
}

The flash works, but afterEnd closure is never called.

I've tried to call

            if repeated <= 1 {      // 1 and not 0, otherwise an extra loop…
                afterEnd?()            // Not called
                return 
            }

in other places, to no avail.

What do I miss ?

Is there a better and simpler way to have a RepeatCount with UIViewPropertyAnimator ?

Answered by Claude31 in 700078022

If anyone interested, here is the extension:

extension UIView {

     // --------------------- flashIt -------------------------------------------------------------
     //  Description: Flash a view a repeated number of times, by changing alpha of the view ; each flash cycle lasts cycleTime
     //  Parameters
     //      repeated: Int       number of flash cycles repeat : 0 = no repeat, just once. 1: repeat once (so 2 flashes)
     //      cycleTime: Double   duration of a cycle
     //      delayed: Double     wait before animation starts
     //      afterEnd: (() -> Void)? = nil  Execute at the end of flashIt animation
     //  -------------------------------------------------------------------------------------------------

     func flashIt(repeated: Int, cycleTime: Double, delayed: Double = 5.0, afterEnd: (() -> Void)? = nil) { 

         if repeated < 0 { return }
         let initAlpha = self.alpha
         
         let animator = UIViewPropertyAnimator(duration: cycleTime, curve: .linear) {
             self.alpha = 0.1
         }
         
          animator.addCompletion { _ in
               // reverse animation 
               let reverseAnimator = UIViewPropertyAnimator(duration: cycleTime, curve: .linear) {
                    self.alpha = initAlpha
               }
               
               reverseAnimator.addCompletion { [self] _ in
                    flashIt(repeated: repeated-1, cycleTime: cycleTime, delayed: 0, afterEnd: afterEnd) // loop, without delay
                    if repeated <= 0 {
                         afterEnd?() // Execute at the end of animation.
                         return
                    }
               }
               reverseAnimator.startAnimation()
          }
         animator.startAnimation(afterDelay: delayed)

     }

Error was that I needed to call the afterEnd here:

            reverseAnimator.addCompletion { [self] _ in
                flashIt(repeated: repeated-1, cycleTime: cycleTime, delayed: 0, afterEnd: afterEnd) // without delay here
            }
Accepted Answer

If anyone interested, here is the extension:

extension UIView {

     // --------------------- flashIt -------------------------------------------------------------
     //  Description: Flash a view a repeated number of times, by changing alpha of the view ; each flash cycle lasts cycleTime
     //  Parameters
     //      repeated: Int       number of flash cycles repeat : 0 = no repeat, just once. 1: repeat once (so 2 flashes)
     //      cycleTime: Double   duration of a cycle
     //      delayed: Double     wait before animation starts
     //      afterEnd: (() -> Void)? = nil  Execute at the end of flashIt animation
     //  -------------------------------------------------------------------------------------------------

     func flashIt(repeated: Int, cycleTime: Double, delayed: Double = 5.0, afterEnd: (() -> Void)? = nil) { 

         if repeated < 0 { return }
         let initAlpha = self.alpha
         
         let animator = UIViewPropertyAnimator(duration: cycleTime, curve: .linear) {
             self.alpha = 0.1
         }
         
          animator.addCompletion { _ in
               // reverse animation 
               let reverseAnimator = UIViewPropertyAnimator(duration: cycleTime, curve: .linear) {
                    self.alpha = initAlpha
               }
               
               reverseAnimator.addCompletion { [self] _ in
                    flashIt(repeated: repeated-1, cycleTime: cycleTime, delayed: 0, afterEnd: afterEnd) // loop, without delay
                    if repeated <= 0 {
                         afterEnd?() // Execute at the end of animation.
                         return
                    }
               }
               reverseAnimator.startAnimation()
          }
         animator.startAnimation(afterDelay: delayed)

     }

You can replace usage of UIView.setAnimationRepeatCount with UIView.modifyAnimations(withRepeatCount:autoreverses:animations:).

func flashIt(repeated: Int, cycleTime: Double, delayed: Double = 5.0, afterEnd: (() -> Void)? = nil) { 

    if repeated < 0 { return }
    let initAlpha = self.alpha
    UIView.animate(withDuration: cycleTime, 
        delay: delayed,
        options:[.allowUserInteraction, .curveEaseInOut],
        animations: {
            UIView.modifyAnimations(withRepeatCount:CGFloat(repeated) autoreverses:true) {
                self.alpha = 0.1    // Not 0.0, to allow user interaction
            }
    },
        completion: { (done: Bool) in
            self.alpha = initAlpha
            afterEnd?() 
    } )
}
repeatCount in UIViewPropertyAnimator
 
 
Q