How to make UIViewControllerContextTransitioning modular for UINavigationControllerDelegate? - ios

I want to create a UIViewController which has always the same in and out transitions applied, without side-effects on all other UIViewController transitions.
Current state
I created a custom transition:
class FadeTransition: UIViewControllerAnimatedTransitioning {
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.45
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVc = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVc = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
transitionContext.completeTransition(false)
return
}
toVc.view.alpha = 0.0
transitionContext.containerView.addSubview(toVc.view)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
toVc.view.alpha = 1.0
}, completion: {_ in
transitionContext.containerView.addSubview(toVc.view)
fromVc.view.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
and then apply it to the given VC:
class CustomTransitionVc: UIViewController, UINavigationControllerDelegate {
let transition = FadeTransition()
init() {
// self.navigationController?.delegate = self // 1. cannot work
Coordinator.shared.navigationController.delegate = self // 2. workaround
}
// MARK: - UINavigationControllerDelegate
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transition
}
Problem
The transition is applied to any view controller pushed and popped in the navigation stack as long as the delegate (the CustomTransitionVc) is not deinit'd.
Questions
I would like the transition only be applied to the current view controller being pushed and popped. Making it more modular and avoiding overlaps of different view controller setting the navigationController.delegate. I can't figure out if this UIKit structure is a bit badly designed (forcing a centralized approach to something that should be modular) or i am using it wrong (or both). Do i have to take care of applying the correct transition to the correct view controller in a centralized place and removing r reapplying the delegate for each screen? Or is there a pattern to reset the delegate in the transition itself?
Is it normal that in order for the transition to be applied when the view controller is pushed (and not only when it pushes another VC) i have to set the delegate in the init() method and not in the viewDidLoad() (it does not work), but at the same time the self.navigationController is not yet available in the init() so i have to use an external reference?
(Bonus) If you could take a look at the implementation of the animation itself would also be great.
--
Docs UIViewcontrollerAnimatedTransitioning
Docs UINavigationDelegate

Related

All view is disappear from view hierarchy after animate transitioning using UIViewControllerContextTransitioning

https://i.stack.imgur.com/kqKLf.gif
Problem:
When implementing the Present transition using UIViewControllerAnimatedTransitioning and UIViewControllerInteractiveTransitioning, if modalPresentationStyle is not .fullScreen, the return of the view(forKey: .from) method of UIViewControllerContextTransitioning is nil.
In the case of Dismiss, on the contrary, the return of view(forKey: .to) is nil. So, if I use the viewController view returned by viewController(forKey: .to) for animation, when the transition is complete, nothing remains in the view layer, and a black screen is displayed.
SomePresentingViewController.swift
let somePresentedViewController = SomePresentedViewController()
somePresentedViewController.transitioningDelegate = somePresentedViewController.transitionController
self.present(somePresentedViewController, animated: true, completion: nil)
SomePresentedViewController.swift
class SomePresentedViewController: UIViewController {
var transitionController = TransitionController()
#IBAction func closeButtonTapped(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
...
TransitionController.swift
class TransitionController: NSObject, UIViewControllerTransitioningDelegate {
let animator: SlideAnimator
override init() {
animator = SlideAnimator()
super.init()
}
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
animator.isPresenting = true
return animator
}
func animationController(
forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
animator.isPresenting = false
return animator
}
}
SlideAnimator.swift
class SlideAnimator: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting: Bool = true
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
print(transitionContext.containerView)
if isPresenting {
animateSlideUpTransition(using: transitionContext)
} else {
animateSlideDownTransition(using: transitionContext)
}
}
func animateSlideDownTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
let container = transitionContext.containerView
let screenOffDown = CGAffineTransform(translationX: 0, y: container.frame.height)
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0.0,
options: .curveEaseInOut,
animations: {
fromVC.view.transform = screenOffDown
}) { (success) in
fromVC.view.transform = .identity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
...
}
Information gathering:
iOS13 bug? In the link below, you can see that the radar for the issue is open to this day.
http://www.openradar.me/radar?id=4999313432248320
There have been many comments that this is a bug in iOS 13 on stackoverflow. But there have been some more compelling comments that this is Apple's intention and why:
https://stackoverflow.com/a/25901154/5705503
A possible cause:
If NO is returned from -shouldRemovePresentersView, the view associated
with UITransitionContextFromViewKey is nil during presentation. This
intended to be a hint that your animator should NOT be manipulating the
presenting view controller's view. For a dismissal, the -presentedView
is returned.
Why not allow the animator manipulate the presenting view controller's
view at all times? First of all, if the presenting view controller's
view is going to stay visible after the animation finishes during the
whole presentation life cycle there is no need to animate it at all — it
just stays where it is. Second, if the ownership for that view
controller is transferred to the presentation controller, the
presentation controller will most likely not know how to layout that
view controller's view when needed, for example when the orientation
changes, but the original owner of the presenting view controller does.
-Apple Documentation Archive-
https://developer.apple.com/library/archive/samplecode/CustomTransitions/Listings/CustomTransitions_Custom_Presentation_AAPLCustomPresentationController_m.html
Attempts to solve:
Set modalPresentationStyle to .fullScreen.
I don't pick this solve because the background of the present view controller must be translucent and the view controller behind it must be visible.
Create a new project to see if the problem recurs
Problem reproduced!
Build on iOS12
Problem reproduced
Subclass UIPresentationController to override shouldRemovePresentersView property to return false and adopt it as presentationController.
I tried the solution mentioned in the link below, but the results were not different.
https://stackoverflow.com/a/41314396/5705503
The role of shouldRemovePresentersView as I know it:
Indicates whether the view of the view controller being switched is removed from the window at the end of the presentation transition.
Substitute the view of the view controller returned by viewController(forKey:) to use it for animation and add the view to the key window.
I was able to achieve the desired result, but it was a bad approach and I did not adopt it as a workaround as it could cause various problems in the future.
Environments:
iOS 13.5 simulator, iOS 13.5.1 iPhoneXS
Xcode 11.5 (11E608c)
Addition)
I found an old Sample where the same problem occurs (black screen is visible when dismiss transition is complete) in the same situation as me.
Please, I hope someone can clone this Sample and run it and let me know how to fix this.
https://www.thorntech.com/2016/03/ios-tutorial-make-interactive-slide-menu-swift/

Basic problem with Custom transition using Navigation controller

In my work on an iOS multi-screen App, I am trying to get my first custom animated transaction to work. Instead of using standard animations when presenting a screen modal and fullscreen, I want to have a horizontal slide in animation.
I have made this work by overriding perform in UIStoryboardSegue, and make the animation and transaction be performed from here.
Overall, it works, except from one case, which is when a navigation controller embeds a view.
In that case, I have followed a tutorial showing precisely how custom transaction is achieved by implementing the UINavigationControllerDelegate protocol for the particular navigation controller.
import Foundation
import UIKit
class AnimatedNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return NavigationControllerTransition()
}
}
Unfortunately the delegate, which should return an animation object:
navigationController(_:animationControllerFor:from:to:)
is never called, and that is my primary problem.
In the tutorial mentioned, a button is used to perform a call to the navigation controller that pushes the destination view with animation set to true:
navigationController.pushViewController(view, animated:true)
In my case, I suspect that the problem is that the navigation controller pushes the view without having the animation parameter set to true, and that explains why the transaction delegate is never called.
(a confirmation off this theory will of course help a bit).
Consequently, I have tried to implement a custom segue that presents the view by calling the push function with animation set to true.
Sadly, I have not been able to get this to work.
When the navigationController.pushViewController(view, animated:true) is called, it raises an exception saying that view has already been pushed.
I have tried a lot without any luck. Code looks like this:
class NavigationControllerSegue: UIStoryboardSegue {
override func perform() {
let navigationController = self.destination as! UINavigationController
let view = navigationController.viewControllers.first!
navigationController.pushViewController(view, animated: true)
}
}
Hey first of all we are talking about two different animations... the first "UIStoryboardSegue" creates a custom subclass UIStoryboardSegue, and this is assigned to the following in question set an id to the following, below just call from the 'ViewController' a self.performSegue(withIdentifier: "YOUR-ID", sender: nil), Example of the subclass UIStoryboardSegue:
import UIKit
// Example =>
class ScaleSegue: UIStoryboardSegue {
override func perform() {
self.scale()
}
func scale() {
let toViewController = self.destination
let fromViewController = self.source
let containerView = fromViewController.view.superview
let originalCenter = fromViewController.view.center
//let originalCenter = self.posizioneClick
toViewController.view.transform = CGAffineTransform.init(scaleX: 0.05, y: 0.05)
toViewController.view.center = originalCenter
containerView?.addSubview(toViewController.view)
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseInOut, animations: {
toViewController.view.transform = CGAffineTransform.identity
}, completion: { success in
fromViewController.present(toViewController, animated: false, completion: nil)
})
}
}
Now, you have to create a segue from storyboard and set as custom class of the segue from the AttributeInspector:
Now to use this you just need to call from the view controller => self.performSegue(withIdentifier: "segue1", sender: nil). Different is the way to implement Transition with UIViewControllerTransitioningDelegate.
If you need to let me know, I can give you examples using this technique too, so long.

Presenting View controller disappears after animation

I've a TabBarController in which presents a view controller with custom animations with UIViewControllerAnimatedTransitioning;
The animations run normally without issues, but after the animationController(forPresented) function runs, the Presenting view controller disappears.
I've found a question around here with people having the same issues but none of those tries solved my issue.
I've read that there is a bug in iOS and we should had again the 'vanished' view controller to the stack, but adding this with UIApplication.shared.keyWindow?.addSubview(presentingView) makes the view added on top of the presentedView and I don't know it adding it again, adds another one to the stack, because it could only be a graphical bug and the view is still part of the container.
Here's some code:
// Global var
var transition = Animator()
// Present a VC modally using tab bar
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController is NewPostVC {
if let newVC = tabBarController.storyboard?.instantiateViewController(withIdentifier: "newPostVC") as? NewPostVC {
newVC.transitioningDelegate = self
newVC.interactor = interactor // new
tabBarController.present(newVC, animated: true)
return false
}
}
return true
}
// Handles the presenting animation
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitioningMode = .Present
return transition
}
// Handles the dismissing animation
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitioningMode = .Dismiss
return transition
}
// interaction controller, only for dismissing the view;
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
//*****************************
/// On the Animator class:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
// Animates to present
if transitioningMode == .Present {
// Get views
guard
let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to),
let presentingView = transitionContext.view(forKey: UITransitionContextViewKey.from)
else {
print("returning")
return
}
// Add the presenting view controller to the container view
containerView.addSubview(presentingView)
// Add the presented view controller to the container view
containerView.addSubview(presentedView)
// Animate
UIView.animate(withDuration: presentDuration, animations: {
presentingView.transform = CGAffineTransform.identity.scaledBy(x: 0.85, y: 0.85);
presentingView.layer.cornerRadius = 7.0
presentingView.layer.masksToBounds = true
presentedView.transform = CGAffineTransform.identity.scaledBy(x: 0.6, y: 0.6);
presentedView.layer.masksToBounds = true
}, completion: { _ in
// On completion, complete the transition
transitionContext.completeTransition(true)
//UIApplication.shared.keyWindow?.addSubview(presentingView)
})
}
// Animates to dismiss
else {
// TODO: - Implement reverse animation
}
}
Note that the animations itself are just tests I'm doing, just scaling them around.
Thx.
The idea is that you don't need to add subview .from during presentation and .to during dismissal to the contatiner's view hierarchy. And if you want to see background underneath, just set the contatiner's view background color to .clear.
After reading Apple's related documentation here I found that it's not a bug that the presentingViewController disappears from the screen, it's just how the API works.
Anyone using transmission animation read the documentation, which was updated and you'll find really interesting and solid explanations there.

iOS 8.4 + Swift - Custom Transitions from UINavigationController, how can I limit the behavior to only one transition in the Navigation Stack?

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.

Custom Storyboard Segue Works in Simulator but not on device

I keep getting the error:
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Push segues can only be used when the source controller is managed by an instance of UINavigationController.'
When trying to switch to a new view controller. Below is the segue switching view controllers:
Even when I try put the name of my transition class in the Segue class, it still gives the error on my device, but works perfectly fine in the simulator.
The code for the transition class:
class TransitionManager: UIStoryboardSegue, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
private var presenting = true
// MARK: UIViewControllerAnimatedTransitioning protocol methods
// animate a change from one viewcontroller to another
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// get reference to our fromView, toView and the container view that we should perform the transition in
let container = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// set up from 2D transforms that we'll use in the animation
let π : CGFloat = 3.14159265359
let offScreenRight = CGAffineTransformMakeRotation(-π/2)
let offScreenLeft = CGAffineTransformMakeRotation(π/2)
// prepare the toView for the animation
toView.transform = self.presenting ? offScreenRight : offScreenLeft
// set the anchor point so that rotations happen from the top-left corner
toView.layer.anchorPoint = CGPoint(x:0, y:0)
fromView.layer.anchorPoint = CGPoint(x:0, y:0)
// updating the anchor point also moves the position to we have to move the center position to the top-left to compensate
toView.layer.position = CGPoint(x:0, y:0)
fromView.layer.position = CGPoint(x:0, y:0)
// add the both views to our view controller
container.addSubview(toView)
container.addSubview(fromView)
// get the duration of the animation
// DON'T just type '0.5s' -- the reason why won't make sense until the next post
// but for now it's important to just follow this approach
let duration = self.transitionDuration(transitionContext)
// perform the animation!
// for this example, just slid both fromView and toView to the left at the same time
// meaning fromView is pushed off the screen and toView slides into view
// we also use the block animation usingSpringWithDamping for a little bounce
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.8, options: nil, animations: {
// slide fromView off either the left or right edge of the screen
// depending if we're presenting or dismissing this view
fromView.transform = self.presenting ? offScreenLeft : offScreenRight
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
// return how many seconds the transiton animation will take
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.75
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
// remmeber that an animator (or animation controller) is any object that aheres to the UIViewControllerAnimatedTransitioning protocol
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// these methods are the perfect place to set our `presenting` flag to either true or false - voila!
self.presenting = true
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = false
return self
}
override func perform() {
//
}
}
Is there anything I am missing? Or how can I get this to work?
Usually that error indicates that you're trying to perform a push segue, and the presenting view controller is not managed by a UINavigationController. The simple solution is usually to embed the presenting (i.e. from) view controller in a navigation controller as outlined in NSGenericException', reason: 'Push segues can only be used when the source controller is managed by an instance of UINavigationController.
But since you've rolled your own UIStoryboardSegue class, I suspect you aren't trying to simply use the default push segue animation. Without knowing how you are managing your transitioningDelegate, my guess is you need to actually override perform() to present your view controller instead of pushing it:
override func perform() {
let fromVC = sourceViewController as UIViewController
let toVC = destinationViewController as UIViewController
fromVC.presentViewController(toVC as UIViewController, animated: true, completion: nil)
}

Resources