UIViewPropertyAnimator state inside completion block? - ios

Playing around with UIViewPropertyAnimator, I am stuck in a weird problem.
Here,
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
animator.addAnimations(someAnimationBlock)
animator.addCompletion{ position in
switch position
{
case .current:
print("is current")
case .start:
print("is start")
case .end:
print("is end")
print(self.animator.state == .active)
print(self.animator.state == .inactive)
print(self.animator.state == .none)
print(self.animator.state == .stopped)
}
}
animator.startAnimation()
After this code runs, this comes to the completion block at .end position but all states are false for animator. Why is this happening ?
To my knowledge it should be at .inactive state as the animations got completed naturally.
P.s I was in the completion block trying to fire animator.startAnimation() and the completion block wasn't getting called anymore. I got to know that the startAnimation() works only when the animator's state is .inactive. So here I tested, and I am receiving no value for state, Need help!

When the animation's completion block is called and the position is .end the animation has completely finished and will soon be removed from the view.
Even if you were to call startAnimation again it's not going to have an effect because the animation is going to go away very soon anyway.
Instead of calling startAnimation from the completion block, you may be able to create new animations on the view and start those.

Related

Dismissal completion callback is executed after custom view controller transition is cancelled

I'm trying to build a custom view controller transition which is interactive and interruptible with these APIs:
UIViewControllerAnimatedTransitioning
UIPercentDrivenInteractiveTransition
UIViewControllerTransitioningDelegate
UIViewPropertyAnimator
What I want to achieve is that I can present a view controller modally, and then use UIPanGestureRecognizer to dismiss the presented view controller by dragging it downward. If I release my finger in the upper half of the screen, the transition should be cancelled, otherwise the transition will be completed successfully.
Here is the code about the problem:
func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: presentedViewController.view)
switch gestureRecognizer.state {
case .began:
interacting = true
presentingViewController.dismiss(animated: true) {
print("Dismissal Completion Callback is Called.")
// How can I know the dismissal is successful or cancelled.
}
case .changed:
let fraction = (translation.y / UIScreen.main.bounds.height)
update(fraction)
case .ended, .cancelled:
interacting = false
if (percentComplete > 0.5) {
finish()
} else {
cancel()
}
default:
break
}
}
My code works great on the aspect of UI and interaction, but I don't understand the behavior of function func dismiss(animated flag: Bool, completion: (() -> Void)? = nil).
In the .began case of Pan Gesture, presentingViewController.dismiss(animated: true) { ... } is called, so the custom transition starts. But the completion callback is always called no mater the dismissal transition is cancelled or not.
I watched these videos of WWDC:
Custom Transitions Using View Controllers
Advances in UIKit Animations and Transitions
They use an example code to demonstrate custom transition with UINavigationController and do not mention the dismissal callback.
presentingViewController.dismiss(animated: true) {
debugPrint("Dismissal Completion Called")
debugPrint("[ presentedViewController.transitionCoordinator?.isCancelled \(self.presentedViewController.transitionCoordinator?.isCancelled) ]")
}
In the document about the completion parameter:
completion
The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
The Question
What is the real meaning of Completion since it is always called after custom transition is cancelled or finished ?
When I use custom transition with presentation and dismissal, what's best practice of handling the real dismissal completion to update UI and data ?
After a little research and testing -- yeah, I'd say this is a bit confusing.
The completion block is NOT called after the VC has been dismissed. Rather, it is called after the function returns.
So, assuming you are implementing UIPercentDrivenInteractiveTransition, .dismiss() triggers your transition code, and returns when you cancel() or finish() -- but its completion block has no knowledge of what you actually did with the transition.
I'm sure there are a various approaches to this... but my first thought would be to put your "completion code" in case .ended, .cancelled: where you are (already) determining whether or not to remove the VC (whether to call .cancel() or .finish()).
Finally, I find something helpful in the document of Apple:
At the end of a transition animation, it is critical that you call the completeTransition: method. Calling that method tells UIKit that the transition is complete and that the user may begin to use the presented view controller. Calling that method also triggers a cascade of other completion handlers, including the one from the presentViewController:animated:completion: method and the animator object’s own animationEnded: method. The best place to call the completeTransition: method is in the completion handler of your animation block.
Because transitions can be canceled, you should use the return value of the transitionWasCancelled method of the context object to determine what cleanup is required. When a presentation is canceled, your animator must undo any modifications it made to the view hierarchy. A successful dismissal requires similar actions.
So the completion callback of present(_:animated:completion:) and dismiss(animated:completion:) do not have any parameters to indicate whether the transition is finished or cancelled. They are both called if the transitionContext.completeTransition(_:) method is called while transition is finished or cancelled. Such behavior is deliberately designed.

Would setting a UIView property explicitly stop/finish an existing animation which was started using UIViewPropertyAnimator on the same property

My current level of iOS dev knowledge is that I am still very new to it and in the learning stage. Giving this info so that experts can answer accordingly :)
As per the Stanford's course for iOS development it was said that an animation for a UIView property would be interrupted if another animation starts for the same property (more specifically the new animation starts working on one of the properties of the previous animation).
Link to the exact point in the video where this was taught https://youtu.be/84ZhYhAwYqo?t=209
The same rather similar thing was also said as an answer to a question on stackoverflow also. Direct link to answer https://stackoverflow.com/a/3076451/8503233
But when I tried this it didn't happen.
To test this I made a simple app where a button click started animating a view using UIViewPropertyAnimator.runningPropertyAnimator, where it changed the alpha to 0. Duration was set to 10 seconds so that I had plenty of time to trigger another animation.
Then another button will start an animation changing the alpha of the same view to 1. I expected that when the second animation will start, it will cause the first animation to stop and its completion handler to be called with the value of UIViewAnimatingPosition as .current.
But what I found that even when the second animation started, the first animation was still running and after running for its full duration, the completion handler was called with UIViewAnimatingPosition as .end.
This is absolutely opposite to what I read in the sources I gave above. Please help. What exactly is happening here. Will share the app code if asked for. Thanks!!!
EDIT1:
View controller code
class MyViewController: UIViewController {
#IBOutlet weak var viewToAnimate: UIView!
#IBOutlet weak var statusView: UIView!
#IBAction func buttonAnimate1(_ sender: UIButton) {
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 5, delay: 0, options: [], animations: {
self.viewToAnimate.alpha = 0.1
}, completion: {position in
switch position {
case .start:
print("In animation 1 completion block with position = start")
case .current:
print("In animation 1 completion block with position = current")
case .end:
print("In animation 1 completion block with position = end")
}
self.statusView.backgroundColor = UIColor.brown
})
}
#IBAction func buttonAnimate2(_ sender: UIButton) {
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 5, delay: 0, options: [.beginFromCurrentState], animations: {
self.viewToAnimate.alpha = 1
}, completion: {position in
switch position {
case .start:
print("In animation 2 completion block with position = start")
case .current:
print("In animation 2 completion block with position = current")
case .end:
print("In animation 2 completion block with position = end")
}
self.statusView.backgroundColor = UIColor.green
})
}
#IBAction func buttonRestore(_ sender: UIButton) {
viewToAnimate.alpha = 1
statusView.backgroundColor = UIColor.yellow
}
}
Test Steps:
In the simulator I press Animate1 button and then after about 1 second, I press Animate2 button.
Output on console:
In animation 1 completion block with position = end
In animation 2 completion block with position = end
In the comments from #matt, what's told is that animations are now additive. This clarifies things a lot but Apple's documentation for addAnimations(_:delayFactor:) says and I quote
If the animation block modifies a property that is being modified by a different property animator, then the animators combine their changes in the most appropriate way. For many properties, the changes from each animator are added together to yield a new intermediate value. If a property cannot be modified in this additive manner, the new animations take over as if the beginFromCurrentState option had been specified for a view-based animation.
So if a new animation takes over as beginFromCurrentState either implicitly or explicitly, then shouldn't the first animation stop with the state as .current when the new animation takes over?
You are consulting ancient history (the answer you cite is from 2010!). It used to be true that with view animation, ordering an animation on a view property already being animated would cancel the existing animation suddenly. But in more recent versions of iOS, view animations have been additive, meaning that a new animation will, by default, be combined smoothly with an existing in-flight animation.

Understanding UIView.animate and how completion closures work

Is there is a way to interrupt animations in a way that doesn't "cancel" (rewind) them, but rather "fast-forwards" them forcing their completion closures to run earlier than originally planned?
Background:
In IOS, one can "animate a view with duration" and include a completion closure as well... using UIView's static method animate() like this:
class func animate(withDuration: TimeInterval, animations: () -> Void, completion: ((Bool) -> Void)? = nil)
A real-life example might look like EXHIBIT-A here:
// assume we have a UILabel named 'bigLabel'
func animationWeNeedToDo() {
UIView.animate(withDuration: 1, animations: {
self.bigLabel.alpha = 0
}, completion: {
if $0 {
UIView.animate(withDuration: 1, animations: {
self.bigLabel.center.x -= 20
}, completion: {
if $0 {
self.updateMainDisplay()
}
}) }
})
}
So we have a UILabel, bigLabel, that we are first animating to "fade," then we are chaining to yet another animation inside the completion of the first, then yet again in the completion of the second, we run the all-important function, updateMainDisplay().
But this simple example could be much more complex involving many more views. It could be imperative that updateMainDisplay() executes. ;)
The updateMainDisplay() function is important because it "resets" all the views, returning the app to a neutral state similar to when the app is originally started... sort of "re-calibrates" everything.
Anyhoo, the trouble is, if the user does something like push the home button early enough or segue to a new activity (modally, like settings... and then come back) while the animation is taking place, it never completes... and so updateMainDisplay() does not get executed! ...and things get complicated and nasty.
So, how to handle this problem?
Seems like something needs to be done in "onPause()" (I know this isn't Android)... like making sure that the animation is cancelled AND that updateMainDisplay() is executed.
But in order to do that you would have to check for all kinds of boolean states in the "onPause()" method. I would much prefer if there were a way to guarantee that the animation will complete.
So, once again, I'm thinking it would be pretty awesome if there were a way to not cancel the animations, but to "force immediate completion" of all animations.
This is pseudo-code... but is there a way to do something like this:
var myAnimation = (animation) { // EXHIBIT-A from above }
myAnimation.execute()
// if needed:
myAnimation.forceCompletionNow()
Does anyone know if that's possible?
Thanks.
The problem with your code is that you are checking the first argument of the completion closure. That indicates whether the animation finishes or not. And you only run updateMainDisplay() if that is true.
So in fact, the completion handler will be called even if the animation is not finished. It is you that told it to do nothing if the animation does not finish.
To fix this, just remove the if $0 statement.
Now Xcode will show a warning because you did not use the first argument of the closure. To silence this warning, just put _ in at the start of the closure:
{ _ in
// some code
}
Another thing that you can try is CABasicAnimation which does not actually change the view's properties. It animates the CALayers. If you update the view again in some way, the view will have gone back to its original state before the animation. You seem to want to reset everything after the animation finishes so this might be suitable for you.

Despite having a delay my function gets executed too quickly

This is my code:
let playableCards = self.allPlayableCardsViews[0].allSubviews.flatMap { $0 as? UIButton }
var counter: Double = 0
for card in playableCards{
UIView.animate(withDuration: 0.3, delay: TimeInterval(counter), options: .init(rawValue: 0), animations: {
card.alpha = 1.0
print("hello")
}, completion: nil)
counter += (3.7/Double(16))
}
Normally in the print line there is a function. This function gets called the amount of loops which of course is good. However I want to add the same delay that is having my card to fade in. Now my function gets called without the delay, causing 16 functions to execute at the exact same time, which is I think weird because I clearly added a delay. I do not want to use completion since the function needs to be executed at the exact same time as the card fades in. How can it be that the card is fading in one after another and the function(print in this example) gets called without delays?
I now see in my debug session 16 times "hello" while the cards are still fading in.
Thank you.
The animation is delayed, but the block can be called at any time to figure out what properties are being animated -- these don't need to be at the same time.
Use a timer to call your function at the same time as the animation will go off. If you want it to be triggered by the animation actually happening, you may be able to use key-value observing (KVO) on the card.alpha property.

sound is not playing when node is touched in spritekit swift

I want to have a sound when a node is clicked.
currently the code is:
let sndButtonClick = SKAction.playSoundFileNamed("button_click.wav", waitForCompletion: false)
and from touches began its
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touches: AnyObject in touches {
let location = touches.locationInNode(self)
if playButton.containsPoint(location) {
playButton.runAction(sndButtonClick)
playButton.removeFromParent()
let nextScene = GamePlayMode(size: self.scene!.size)
nextScene.scaleMode = self.scaleMode
self.view?.presentScene(nextScene)
}
}
I did the exact same thing for the collision of two nodes in gameplaymode and it works but in main menu it does not work!
I tried
self.runAction(sndButtonClick)
then
playbutton.runAction(sndButtonClick)
both didnt work
Why running sound action on a button doesn't work ?
This is from the docs:
An SKAction object is an action that is executed by a node in the
scene (SKScene)...When the scene processes its nodes, actions associated with those nodes are evaluated.
Means the actions for a node will run only if the node is added to the scene. So this has no effect :
playButton.runAction(sndButtonClick)
playButton.removeFromParent()
because you have removed the button from the scene and it will not be present in the next frame when actions should be executed. That is how runAction method works:
Adds an action to the list of actions executed by the node...The new action is processed the next time the scene’s animation loop is processed.
Also, because you are immediately calling presentScene there will be no next frame anyways, so even if you delete removeFromParent statement, sound will not work, because there is no next frame.
Why running sound action on a scene doesn't work?
self.runAction(sndButtonClick) won't work because you are making a transition immediately without waiting for a next frame where the queued actions will be executed (like described above).
Solution for the Problem
To play the sound before transition, you have to wait for a next frame, and you can do something like:
runAction(sndButtonClick, completion: {
self.view?.presentScene(nextScene)
})
or
let block = SKAction.runBlock({
self.view?.presentScene(nextScene)
})
runAction(SKAction.sequence([sndButtonClick, block]))
Preventing Leaks:
Consider using capture list inside of a block which captures self to avoid possible strong reference cycles when needed, like this:
let block = SKAction.runBlock({
[unowned self] in
//use self here
})
In this particular case of yours, it should be safe to go without capture list because scene doesn't have a strong reference to the block. Only block has strong reference to the scene, but after the block is executed, because nothing retains it (no strong references to it), it will be released, thus the scene can be released correctly. But, if the block was declared as a property, or the action which executes the block was running infinitely (using repeateActionForever method to repeat a certain sequence), then you will have a leak for sure.
You should always override scene's deinit to see what is going on (if it is not called, something retaining the scene and causing the leak).

Resources