Search code examples
swiftbluruivisualeffectviewuiviewpropertyanimator

Animate the fractionComplete of UIViewPropertyAnimator for blurring the background


So I'm using the new UIViewPropertyAnimator and UIVisualEffectView to achieve the same thing as the Spotlight search when you scrolling down on the home screen and it blurs the background.

I'm using the fractionComplete property to set the procent of how much to blur when panning a UIView.

    animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
        self.blurEffectView.effect = nil
    }

And the amount of blurriness is changed with a value between 0.0 - 1.0.

        animator?.fractionComplete = blurValue

But when I cancel the pan gesture I want the blur to animate back from where it is to no blur (e.g ~ -> 1.0) with a duration of something like 0.4 milliseconds.

Right now I just set the fractionComplete to 1.0 when the pan gesture is cancelled. Instead I want to animate it.
I have tried the UIView.animate(withDuration.. but it doesn't affect the UIViewPropertyAnimators fractionComplete, and thats the only way to blur an UIVisualEffectView.

Any ideas?


Solution

  • It seems that fractionComplete has a bug (my question on Stackoverflow: UIViewPropertyAnimator does not update the view when expected), rdar://30856746. The property only sets the state from inactive to active, but does not update the view, because (I assume) there is another internal state that does not trigger.

    To workaround the problem you can do this:

    animator.startAnimation() // This will change the `state` from inactive to active
    animator.pauseAnimation() // This will change `isRunning` back to false, but the `state` will remain as active
    
    // Now any call of `fractionComplete` should update your view correctly!
    animator.fractionComplete = /* your value here */
    

    Here is a playground snippet to play around:

    let liveView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 50))
    liveView.backgroundColor = .white
    
    PlaygroundPage.current.needsIndefiniteExecution = true
    PlaygroundPage.current.liveView = liveView
    
    let square = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
    square.backgroundColor = .red
    
    liveView.addSubview(square)
    
    let animator = UIViewPropertyAnimator.init(duration: 5, curve: .linear)
    
    animator.addAnimations {
    
        square.frame.origin.x = 350
    }
    
    let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
    blurView.frame = liveView.bounds
    
    liveView.addSubview(blurView)
    
    animator.addAnimations {
    
        blurView.effect = nil
    }
    
    // If you want to restore the blur after it was animated, you have to 
    // safe a reference to the effect which is manipulated
    let effect = blurView.effect
    
    animator.addCompletion {
        // In case you want to restore the blur effect
        if $0 == .start { blurView.effect = effect }
    }
    
    animator.startAnimation()
    animator.pauseAnimation()
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    
        animator.fractionComplete = 0.5
    }
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
    
        // decide the direction you want your animation to go.
        // animator.isReversed = true
        animator.startAnimation()
    }