I need to animate a UIViewController which is the root of a UINavigationController. This navigation controller is presented modally.
I know I can do it with the lifecycle methods of the view controller, but I want to use a UIViewControllerAnimatedTransitioning if possible to keep the VC code clean.
I tried the following:
a) Set the UINavigationControllerDelegate to the navigation controller, so I could use this delegate function:
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
However, it only gets called during transitions within the navigation controller (from VC to VC).
b) Set a UIViewControllerTransitioningDelegate in the root view controller and use this delegate function:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
But again, it doesn't get called as the one presenting is the navigation controller.
c) Set a UIViewControllerTransitioningDelegate in the navigation controller and use the same delegate function:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
In this case, it gets called and I can inject the UIViewControllerAnimatedTransitioning object, but I can only think about hacks to animate the root, e.g.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) as? UINavigationController,
toVC.children.count == 1,
let myRootVC = toVC.children.first as? MyViewController
else {
transitionContext.completeTransition(false)
return
}
// Animate manually
myRootVC.view.alpha = 0
// [...]
}
Do you know any way to capture this nicely?
I solved the similar problem by making all my ViewControllers being presented as embedded ViewControllers using ViewController Containment.
I do have 1 RootContainerViewController setup when launching the the application - it's simply set as rootViewController without animations and it's just container for all other ViewControllers.
All other ViewControllers are always added as SubViewControllers and SubViews to that container.
That allows you not only to use UIViewControllerAnimatedTransitioning for every single transition in your application, but also it allows you to easily make "reset" of the app, by just removing RootContainerViewController and replacing it with fresh one.
Another advantage is that you can add as many ViewControllers as you want - compared to native presentViewController that allows you to have only 1 ViewController presented at a time.
Any other approaches I tried ended up badly - UIViewControllerAnimatedTransitioning is impossible to use directly with rootViewController changes - you will have to combine it with UIView.animate to support it.
Related
I have a UINavigationController with custom transitions when pushing and popping using the UIViewControllerAnimatedTransitioning protocol:
class NavigationHandler: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimator()
}
}
class CustomAnimator: UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// do animation stuff
}
}
This works fine for push and pop, but it doesn't work for the back-swipe gesture (which will show the default transition). I would like to customize the back swipe, but can't find a good way to do that.
Knowledge I gained so far:
Setting up my own gesture recognizer to detect the back swipe is possible, but UINavigationController will still perform the default animation, so now my animation fights the default animation, which seems wrong
UINavigationControllerDelegate has a method navigationController(:interactionControllerFor animationController:), which seems like it's exactly what I need, but it's only called for regular push/pop operations, not for back swipe
I am using below delegate method to show animation in my app.
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
But, above method is not getting call for rootViewController of navigationController.
Thanks for help in advance.
The method you're referring to is called to set up the transition animation. However, the rootViewController is added to the navigationController without any animation since there is no previous view controller to transition from. As a result, the delegate method is not invoked.
This is true even if you don't use the convenience initializer that specifies the rootViewController and instead push the rootViewController manually, specifying true for the animated: parameter. You'll notice that even in this case, the navigationController pushes the rootViewController without animation (as evidenced from the navigationController(:,willShow:,animated:) call having the animated parameter set to false.
AFAIK, it is not possible to transition-animate the rootViewController.
An alternative solution might be to implement navigationController(:, willShow:, animated:), do a check to see if self.viewControllers.first == viewController and then manually animate the viewController.view.
I need help with custom segue transition from top to bottom. Its easy transition but I don't know how to do it. Is it done in a storyboard or must I do it in code? If it must do programmatically how I do it?
You are trying to write a custom transition animation for a push segue. So this is what you are going to do:
Set a delegate for your navigation controller.
In the delegate, implement func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? to return an object implementing UIViewControllerAnimatedTransitioning (this is often self in real life)
In that object, implement func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval and func animateTransition(using ctx: UIViewControllerContextTransitioning).
In animateTransition, get the info from the context (ctx). Put the to view into the containerView and animate it to its finalFrame. In your case, you'll start with the view above the final frame and animate it downwards, as you've specified. When you are all done (i.e. in the animation's completion handler), be sure to call completeTransition on the context.
I have implemented UINavigationControllerDelegate method:
public func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { }
because I had to set application state and only "event" that was able to capture Pop transition of my UINavigationController was this method. Everything is working properly but as far as that functionality is concerned, but now my swipe back action does not work (only pressing back button works).
At the end of this method I have returned nil because I had no idea what its doing but apparently it affects that swipe to back functionality.
Is there any default UIViewControllerAnimatedTransitioning that I can returned to have that swipe back functionality?
Thanks.
I switched to this delegate method:
public func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if self.viewControllerCount - 1 == self.rootController.viewControllers.count {
// Pop action, do additional logic
}
}
and customized it so that I can know for sure if its pop action (tracking last number of view controllers.
I've been dabbling with some very basic custom transitions recently and I'm starting to get the hang of it. Up until this point, however, I had only made Modal transitions.
I wrote the following code for a basic "Fade" transition from my "Master View" to my "Settings View" (both of which are part of the NavigationController Stack):
import UIKit
import QuartzCore
///Transition manager for transitioning between the loginVC and the content that follows.
class FadeTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let destinationVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let sourceView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let destinationView = transitionContext.viewForKey(UITransitionContextToViewKey)
transitionContext.containerView().addSubview(destinationView!)
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { () -> Void in
sourceView!.alpha = 0.0
destinationView!.alpha = 1.0
}) { (didComplete) -> Void in
println("Fade Transition: \(didComplete)")
transitionContext.completeTransition(didComplete)
}
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.5
}
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
I was excited. I rejoiced. All appeared to be well until I then went to a different View Controller that also has a segue from the "Master View"...and it had the same transition. Of course this is to be expected, I just hadn't thought of it.
How can I limit this transition to only occur between two specific UIViewControllers?
You need to look at the UINavigationControllerDelegate method navigationController(_:animationControllerForOperation:fromViewController:toViewController). You have implemented it in your custom transition class above (I would suggest making a more general class your nav controller delegate instead, such as the view controller that contains it or the app delegate), but you return self indisciminately, indicating that every transition for that navigation controller should use your fade transition.
Instead, look at the from/toViewController parameters to decide whether you want to use a custom transition between those particular view controllers. If you want to use the default animated transition, just return nil.
From the UINavigationControllerDelegate Protocol Reference:
Return Value
The animator object responsible for managing the transition animations,
or nil if you want to use the standard navigation controller
transitions. The object you return must conform to the
UIViewControllerAnimatorTransitioning protocol.