In my app I have a custom interactive transition for some view controllers. I am talking about UINavigationController interactive transitions for push/pop. For all other view controllers, I want to use the default iOS interactive transition (swiping from left to right to go back).
To activate my custom interactive transition I have to implement a UINavigationControllerDelegate and implement the animationControllerFor callback. This works fine! But if I try to return nil for some cases, meaning I don't want my custom transition and instead use the default one, it disables the back swipes completely!
Code to reproduce the issue:
class MainNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationControllerOperation,
from fromController: UIViewController,
to toController: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
}
Use this MainNavigationController and all your back swipes will be disabled inside this navigation controller! I wasn't expecting this. Because the documentation says about the return value:
The animator object responsible for managing the transition animations, or nil if you want to use the standard navigation controller transitions.
https://developer.apple.com/documentation/uikit/uinavigationcontrollerdelegate/1621846-navigationcontroller
Does anyone have any idea whether I am doing something wrong or if this is a bug? I tried this on both iOS 11 and iOS 12 beta.
Related
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.
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 have a custom subclass of UINavigationController that sets itself as the UINavigationControllerDelegate, and conditionally returns a custom animator. I want to be able to toggle between the custom animator and the system animation using a boolean flag. My code looks something like this:
class CustomNavigationController: UINavigationControllerDelegate {
var useCustomAnimation = false
private let customAnimator = CustomAnimator()
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if useCustomAnimation {
return CustomAnimator()
}
return nil
}
}
However, when useCustomAnimation is false, the interactive back gesture managed by the system no longer works. Everything else related to the system animation still works.
I've tried setting the delegate of the interactive pop gesture to my custom navigation controller and returned true/false from some methods with varying levels of success.
So it seems this is a bug in UIKit. I've created a small project that reproduces the error and submitted it to Apple. Essentially the interactive pop gesture is broken whenever the animationController delegate method is implemented by the UINavigationControllerDelegate. As a workaround, I've created two delegate proxies, one that implements the method, and one that does not:
class NavigationControllerDelegateProxy: NSObject, UINavigationControllerDelegate {
weak var delegateProxy: UINavigationControllerDelegate?
init(delegateProxy: UINavigationControllerDelegate) {
self.delegateProxy = delegateProxy
}
/*
... Other Delegate Methods
*/
}
class CustomAnimationNavigationControllerDelegateProxy: NavigationControllerDelegateProxy {
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return delegateProxy?.navigationController?(navigationController,
animationControllerFor: operation,
from: fromVC,
to: toVC)
}
}
I simply alternate between these classes acting as the actual UINavigationControllerDelegate depending on the state of useCustomAnimation.
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 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.