8

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?

alengqvist
  • 491
  • 5
  • 15
  • I have had the same bug, and at this point I think it might be an Apple bug related to the effect being cleared instead of a value that it can transition between 0 and 1. Please make sure to file a radar with Apple. – livings124 Dec 25 '16 at 04:51
  • just to confirm, when you start panning, the animation starts to blur the background. when you stop panning, you want it to go back to original state? – Milan Nosáľ Feb 16 '17 at 17:17

3 Answers3

15

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()
}
Community
  • 1
  • 1
DevAndArtist
  • 4,971
  • 1
  • 23
  • 48
  • Works like a charm! Hopefully there will be a fix in the near future. – alengqvist Mar 20 '17 at 17:06
  • Thanks! Duped the bug. There seem to be even more problems, when you do not do the `startAnimation()`-`pauseAnimation()` dance, e.g. calling `continueAnimation()` does not work without a small delay. – Klaas Jul 07 '17 at 09:39
  • @Klaas right now I'm not even sure if it's a real bug or intended. In WWDC17 Apple engineers always used `pauseAnimation()` without even calling `startAnimation()`, which seems to do the trick here as well. :/ It could also be that they're work around the bug as well. :D – DevAndArtist Jul 07 '17 at 16:27
  • @DevAndArtist I created a little sample project that illustrates the bug that I'm having trouble with: https://github.com/klaas/RadarSample_UIViewPropertyAnimatorBug (It's an Xcode template, only changed `ViewController.swift`). – Klaas Jul 08 '17 at 11:15
  • @Klaas I made the same observation like `flag_useWorkaround2` in my original post, but using `fractionComplete` instead: https://stackoverflow.com/questions/42607833/uiviewpropertyanimator-does-not-update-the-view-when-expected – DevAndArtist Jul 09 '17 at 13:02
  • @DevAndArtist Thanks for having a look. I'll create another radar with my sample code... – Klaas Jul 09 '17 at 13:12
  • I don't think it's a bug. Here's a snippet from the `fractionComplete`'s documentation: `You can update the value of this property only while the animator is paused. Changing the value of this property on an inactive animator moves it to the active state.`. – Iulian Onofrei Nov 21 '18 at 09:52
3

If you're still looking for a way to actually animate fractionComplete without the use of a slider or a gesture, I was quite happy with my results using a CADisplayLink. You can see my results here: https://gist.github.com/vegather/07993d15c83ffcd5182c8c27f1aa600b

vegather
  • 470
  • 4
  • 14
0

I've used a delayed loop to decrease "fractionComplete"

func resetAnimator(){
        
        let duration = 1.0 / 60.0
        
        if self.animator.fractionComplete > 0 {
            self.animator.fractionComplete -= 0.06
            delay(duration, closure: {
                self.resetAnimator()
            })
        }
    }

func delay(_ delay:Double, closure:@escaping ()->()) {
        DispatchQueue.main.asyncAfter(
            deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
    }
Matthew Rodríguez
  • 429
  • 1
  • 3
  • 7