2

Is there any possibility to interrupt a UIView.transition at its current state on iOS?

To give you a bit of context: I have a UIButton where I need to animate the title color - the animation can have different delays attached to it, though. Since UIView.transition does not support delays, I simply delayed execution of the entire block:

DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
    UIView.transition(with: button, duration: 0.4, options: [.beginFromCurrentState, .allowUserInteraction, .transitionCrossDissolve], animations: {
        button.setTitleColor(selected ? .red : .black, for: .normal)
    }, completion: nil)
}

The issue here is that, if this code is executed in quick succession with different delays, the outcome can be unexpected. So for example this could be called with selected=true, delay=0.5 and immediately after that with selected=false, delay=0.0. In this scenario, the button would end up red even though it should be black.

Therefore, I am looking for a method to either have UIView.transform with a delay, interrupt a UIView.transform or make setTitleColor() animatable through UIView.animate.

Kuldeep
  • 4,466
  • 8
  • 32
  • 59
BlackWolf
  • 5,239
  • 5
  • 33
  • 60
  • If I ask you to face right after five minutes and also to face left now, then I'd expect you to be facing right shortly after the five minutes has elapsed. The order in which the requests are made doesn't determine the order in which they're carried out — if it should, then you need to reconsider the design of your system to enforce that behavior. – Caleb Oct 26 '18 at 14:51
  • I'm not saying the behaviour is unexpected, I'm saying it's not the behaviour I'm looking for :-) I need to animate the title color and I know I will have different delays on the animations, and I'm looking for a way to go smoothly from one animation into another in order to arrive at the final state of my button – BlackWolf Oct 26 '18 at 14:54
  • or maybe to simplify it further: I know I will have `UIView.transition()` calls that overlap, and I'm looking for a way to transition between them – BlackWolf Oct 26 '18 at 14:58
  • manually set a timer instead of using asyncAfter? if there are second requests comes in, just invalidate the timer so nothing will happened in that case. – Surely Oct 26 '18 at 17:45
  • that... makes complete sense. I‘m trying to figure out why I didn‘t think of that >< Care to make an answer out of that? – BlackWolf Oct 26 '18 at 18:28
  • The problem here isn't that your animations overlap. The problem is that because of delays that you're assigning, they don't run in the sequence you seem to intend. But it's not clear what solution you want, or why you're assigning these delays if you don't really want them to run that way. Do you want them to run in the order you create them? Then add them to a queue and run them sequentially. – Caleb Oct 27 '18 at 19:00

1 Answers1

2

I actually created a framework to do this specifically, you can trigger animations relative to time progress, or value interpolation progress. Framework and documentation can be found here:

https://github.com/AntonTheDev/FlightAnimator

The following examples assume you a CATextLayer backed view, as this is not a question about how to set that up, here is a small example to get you started:

class CircleProgressView: UIView {
    .....

    override class func layerClass() -> AnyClass {
        return CircleProgressLayer.self
    }
}

Example 1:

Assuming you have a CATextLayer backed view, we can animate the it's foreground color as follows, and trigger another animation relative to time:

var midPointColor = UIColor.orange.cgColor
var finalColor    = UIColor.orange.cgColor
var keyPath       = "foregroundColor"

textView.animate { [unowned self] (a) in
    a.value(midPointColor, forKeyPath : keyPath).duration(1.0).easing(.OutCubic)

    // This will trigger the interruption 
    // at 0.5 seconds into the animation

    animator.triggerOnProgress(0.5, onView: self.textView, animator: { (a) in
        a.value(finalColor, forKeyPath : keyPath).duration(1.0).easing(.OutCubic)
    })
}

Example 2:

Just as in the prior example, once you have a CATextLayer backed view, we can animate it's foregroundColor relative to the progress matrix of the RBG values, calculated based on the magnitude progress of the 3 values:

var midPointColor = UIColor.orange.cgColor
var finalColor    = UIColor.orange.cgColor
var keyPath       = "foregroundColor"

textView.animate { [unowned self] (a) in
    a.value(midPointColor, forKeyPath : keyPath).duration(1.0).easing(.OutCubic)

    // This will trigger the interruption 
    // when the magnitude of the starting point
    // is equal to 0.5 relative to the final point

    a.triggerOnValueProgress(0.5, onView: self.textView, animator: { (a) in
        a.value(finalColor, forKeyPath : keyPath).duration(1.0).easing(.OutCubic)
    })
}

Hope this helps :)

AntonTheDev
  • 899
  • 6
  • 13
  • Great framework! The problem is, though, that title color is *not* an animatable property, so I assume this will not work? – BlackWolf Oct 26 '18 at 11:06
  • Check out the swift 4 branch, which should have fixed color interpolation. You may need to make a CATextLayer, with an animatable property to interpolate over. – AntonTheDev Oct 26 '18 at 11:08
  • @ArnabHore I have added some more context to the answer :) – AntonTheDev Oct 26 '18 at 14:39