Adding a completion handler to UIViewPropertyAnimator in Swift

680 Views Asked by At

I'm trying to get a property animator to start animation when a View Controller is presented. Right now the animation is playing however the UIViewPropertyAnimator doesn't respond to the completion handler added to it.

  • UIVisualEffectView sub-class.
import UIKit

final class BlurEffectView: UIVisualEffectView {
    
    deinit {
    animator?.stopAnimation(true)
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        effect = nil
        animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in
          self.effect = theEffect
        }
        animator?.pausesOnCompletion = true
   }

    private let theEffect: UIVisualEffect = UIBlurEffect(style: .regular)
    var animator: UIViewPropertyAnimator?

}

  • First View controller
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func doSomething(_ sender: UIButton) {
        let vc = storyboard?.instantiateViewController(identifier: "second") as! SecondVC
        vc.modalPresentationStyle = .overFullScreen

        present(vc, animated: false) { //vc presentation completion handler
            
            //adding a completion handler to the UIViewPropertyAnimator
            vc.blurView.animator?.addCompletion({ (pos) in
                print("animation complete") //the problem is that this line isn't executed 
            })
            vc.blurView.animator?.startAnimation()
        }
    }
    
}
  • Second view controller
import UIKit

class SecondVC: UIViewController {

    @IBOutlet weak var blurView: BlurEffectView!

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Here the UIViewPropertyAnimator completion handler is added after the Second View Controller(controller with visual effect view) is presented. I have tried moving the completion handler to different places like viewDidLoad and viewDidAppear but nothing seems to work.

2

There are 2 best solutions below

2
On BEST ANSWER

This whole thing seems incorrectly designed.

draw(_ rect:) is not the place to initialize your animator*, my best guess at what's happening is that vc.blurView.animator? is nil when you try to start it (have you verified that it isn't?).

Instead, your view class could look like this**:

final class BlurEffectView: UIVisualEffectView {
    func fadeInEffect(_ completion: @escaping () -> Void) {
        UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0, options: []) {
            self.effect = UIBlurEffect(style: .regular)
        } completion: { _ in
            completion()
        }
    }
}

And you would execute your animation like this:

present(vc, animated: false) { //vc presentation completion handler
    vc.blurView.fadeInEffect {
        // Completion here
    }
}

*draw(_ rect:) gets called every time you let the system know that you need to redraw your view, and inside you're supposed to use CoreGraphics to draw the content of your view, which is not something you're doing here.

**Since you're not using any of the more advanced features of the property animator, it doesn't seem necessary to store it in an ivar.

0
On

The problem is that you are setting pausesOnCompletion to true. This causes the completion handler to not be called.

If you actually need that to be set to true, you need to use KVO to observe the isRunning property:

// property of your VC
var ob: NSKeyValueObservation?

...

self.ob?.invalidate()
self.ob = vc.blurView.animator?.observe(\.isRunning, options: [.new], changeHandler: { (animator, change) in
    if !(change.newValue!) {
        print("completed")
    }
})
vc.blurView.animator?.startAnimation()

And as EmilioPelaez said, you shouldn't be initialising your animator in draw. Again, if you actually have a reason for using pausesOnCompletion = true, set those in a lazy property:

lazy var animator: UIViewPropertyAnimator? = {
    let anim = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in
        self.effect = self.theEffect
    }
    anim.pausesOnCompletion = true
    return anim
}()

self.effect = nil could be set in the initialiser.