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.
Related
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.
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.
I am writing a chess GUI in Swift 3 and use nvzqz/Sage as the chess model/library. Now I face a problem with a Sage closure used for piece promotion.
Sage uses (in its game class) the execute(move: promotion:) method for promotion move execution which has a closure that returns a promotion piece kind. This allows to prompt the user for a promotion piece or perform any other operations before choosing a promotion piece kind, as follows:
try game.execute(move: move) {
...
return .queen
}
I implemented a promotion view controller ("pvc") which is called in this closure so that the player may select the new piece:
// This is in the main View Controller class
/// The piece selected in promotionViewController into which a pawn shall promote
var newPiece: Piece.Kind = ._queen // default value = Queen
try game.execute(move: move) {
boardView.isUserInteractionEnabled = false
// promotionview controller appears to select new promoted piece
let pvc = PromotionViewController(nibName: nil, bundle: nil)
pvc.delegate = self
pvc.modalPresentationStyle = .overCurrentContext
self.present(pvc, animated:true)
return newPiece
}
When the button for the new piece in the pvc is pressed, the pvc dismisses itself and the data of the selected piece (the constant selectedType) is transferred back to the main view controller via delegation:
// This is in the sending PVC class
protocol PromotionViewControllerDelegate {
func processPromotion(selectedType: Piece.Kind)
}
func buttonPressed(sender: UIButton) {
let selectedType = bla bla bla ...
delegate?.processPromotion(selectedType: selectedType)
presentingViewController!.dismiss(animated:true)
}
// This is in the receiving main View Controller class
extension GameViewController: PromotionViewControllerDelegate {
func processPromotion(selectedType: Piece.Kind) {
defer {
boardView.isUserInteractionEnabled = true
}
newPiece = selectedType
}
The problem I have is that the closure (in the game.execute method) does not wait until the player made his selection in the pvc (and immediately returns the newPiece variable which is still the default value) so that I never get another promotion piece processed other than the default value.
How do I make the closure wait until the player pressed a button in the pvc?
Of course, I tried to find a solution and read about callbacks, completion handlers or property observers. I do not know which is the best way forward, some thoughts:
Completion handler: the pvc dismisses itself upon button-press event so the completion handler is not in the receiving (main) view controller. How do I deal with this?
Property observer: I could call the try game.execute(move) method only after the promotion piece was set (with didset) but that would make the code difficult to read and not use the nice closure the game.execute method provides.
Callbacks may be related to completion handlers, but am not sure.
So your block in game.execute(move: move) will fully execute which is so designed by the Sage API. You can not pause it as easy but it is doable, still let's try to solve it the other way;
Why do you need to call the presentation of the view controller within this block? By all means try to move that away. The call try game.execute(move: move) { should only be called within processPromotion delegate method. You did not post any code but wherever this try game.execute(move: move) { code is it needs to be replaced by presenting a view controller alone.
Then on delegate you do not even need to preserve the value newPiece = selectedType but rather just call try game.execute(move: move) { return selectedType }.
So about pausing a block:
It is not possible to directly "pause" a block because it is a part of execution which means the whole operation needs to pause which in the end means you need to pause your whole thread. That means you need to move the call to a separate thread and pause that one. Still this will only work if the API supports the multithreading, if the callback is called on the same tread as its execute call... So there are many tools and ways on how to lock a thread so let me just use the most primitive one which is making the thread sleep:
var executionLocked: Bool = false
func foo() {
DispatchQueue(label: "confiramtion queue").async {
self.executionLocked = true
game.execute(move: move) {
// Assuming this is still on the "confiramtion queue" queue
DispatchQueue.main.async {
// UI code needs to be executed on main thread
let pvc = PromotionViewController(nibName: nil, bundle: nil)
pvc.delegate = self
pvc.modalPresentationStyle = .overCurrentContext
self.present(pvc, animated:true)
}
while self.executionLocked {
Thread.sleep(forTimeInterval: 1.0/5.0) // Check 5 times per second if unlocked
}
return self.newPiece // Or whatever the code is
}
}
}
Now in your delegate you need:
func processPromotion(selectedType: Piece.Kind) {
defer {
boardView.isUserInteractionEnabled = true
}
newPiece = selectedType
self.executionLocked = false
}
So what happens here is we start a new thread. Then lock the execution and start execution on game instance. In the block we now execute our code on main thread and then create an "endless" loop in which a thread sleeps a bit every time (the sleep is not really needed but it prevents the loop to take too much CPU power). Now all the stuff is happening on main thread which is that a new controller is presented and user may do stuff with it... Then once done a delegate will unlock the execution lock which will make the "endless" loop exit (on another thread) and return a value to your game instance.
I do not expect you to implement this but if you will then ensure you make all precautions to correctly release the loop if needed. Like if view controller is dismissed it should unlock it, if a delegate has a "cancel" version it should exit...
I am using Swift 3, Xcode 8.2.
I have a view controller on which there is a button. When the button is pressed, it runs the buttonPressed function below. It kicks off an asynchronous job which takes a few seconds to complete during which a spinning gear activity indicator pops up. Once the job is finished, I want the activity indicator to dismiss and the view controller to dismiss as well to the VC that it originally came from.
func buttonPressed() {
// Set up some stuff here
...
// this code block here is to asynchronously run the processing job
// while the activity indicator gear spins
self.displaySpinningGear()
DispatchQueue.main.async {
self.doSomeJobProcessing()
}
self.dismiss(animated: true, completion: nil)
// also dismiss the camera view
self.presentingViewController?.dismiss(animated: true, completion: nil) // error here
}
However, I get an error on that last statement:
[Assert] Trying to dismiss the presentation controller while
transitioning already.
(<_UIAlertControllerAlertPresentationController: 0x109e3b190>)
2017-03-03 21:37:56.833899 EOB-Reader[27710:6869686] [Assert]
transitionViewForCurrentTransition is not set, presentation controller
was dismissed during the presentation?
(<_UIAlertControllerAlertPresentationController: 0x109e3b190>)
Any help would be greatly appreciated.
Perform anyone transition of these at a time.
if let viewController = presentingViewController {
// This block will dismiss both current and a view controller presenting current.
viewController.dismiss(animated: true, completion: nil)
} else {
// This block will dismiss only current view controller
self.dismiss(animated: true, completion: nil)
}
Two transition operations cannot be performed simultaneously.
In your source code you are dismissing current view controller with animation. Now animation generally takes time around 0.25 second to complete its operation.
Now, in next line you are trying to dismiss a ( ** ) view controller that has presented current view controller. (In your case ( ** ) view controller is also presented by some other view controller.) so, within a fraction of seconds/milliseconds ( ** ) view controller will also try to dismiss it self with animation.
At this time your current view controller is being dismissed and (**) view controller will also start dismiss operation. So, both operations conflict each other on main executing thread. And results into an issue, you are facing.
Also, share your code for block
self.doSomeJobProcessing()
Share here, if you've set any other view/controller transition operations here.
Remember that when you dispatch asynchronous you're typically dispatching to background thread to prevent UI blocking. All on screen (UI) components need be modified on the main thread. So start your progress spinner from the main thread, dispatch your heavy lifting to the background, then redispatch to the main thread that you'd like to. this.dismissviewcontroller:animated
Will update with code shortly.
I'm using snapshot testing for my view controller. This is how the view controller is initialized in tests:
window.addSubview(viewController.view) // simulate the view is visible (probably unnecessary)
viewController.view.frame = self.snapshotFrame // set frame
viewController.beginAppearanceTransition(true, animated: false) // simulate VC's life cycle
viewController.endAppearanceTransition()
My view controller contains UICollectionView. When I perform collection view updates using performBatchUpdates, even though the update block is finished, the completion is never called.
// Animate udpates
self.collectionView.performBatchUpdates({
// is called
}, completion: { _ in
// never called
})
I think it's related to the off screen rendering of the collection view. Does someone have any experience with similar issue? What am I missing to convince UICollectionView that it's on screen?
I found the problem. It was all about a proper timing. The test case finished before the completion was called and the view controller was deallocated.
I speeded up collection view animations by setting
viewController.view.layer.speed = 100 // speed up animations
and increased timeout for the test case to 0.1 seconds. Everything works as expected now.