Custom transitions between view controllers supporting different/mismatching orientations - ios

Specifically, I have a problem with dismissing view controller B so that view controller A is visible again. Following the steps below, the appearance of view controller A will be heavily distorted.
View controller A supports all orientations but upside down.
View controller B supports portrait only.
How to cause the problem/bug.
Hold VC A in portrait.
Open VC B.
Hold device in landscape.
Dismiss VC B.
The appearance of VC A is now heavily broken/distorted/buggy.
I am suspecting the same problem would appear if the VCs support another set of orientations, but for now this is the exact and simple case where the problem occurs.
The transition is handled by a class implementing UIViewControllerAnimatedTransitioning. It basically looks like this.
class FadeAnimator: NSObject {
let duration: TimeInterval = 1.0
var originFrame = CGRect.zero
}
extension FadeAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return self.duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
containerView.addSubview(toView)
toView.alpha = 0
UIView.animate(withDuration: self.duration, animations: {
toView.alpha = 1
}, completion: { _ in
transitionContext.completeTransition(true)
})
}
}
Full sample project which shows the problem is here: https://bitbucket.org/zzzzapjonny/testtransition/src/master/testtransition/

Related

How to make UIViewControllerContextTransitioning modular for UINavigationControllerDelegate?

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

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/

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.

segueForUnwindingToViewController causing "Warning: Attempt to present <...> on <...> which is already presenting <...>"

I am building an app and recently discovered a huge memory leak caused by traditional segues.
Therefore I learned about unwind segue. Everything works just fine if I simply use:
#IBAction func prepareForUnwindToMainFromFriends(segue: UIStoryboardSegue) {
}
Memory leak is solved and 'everything is awesome'. But this solution looks ugly on a UI point of view. So I implemented this function from this website. And changed it a little.
override func segueForUnwindingToViewController(toViewController: UIViewController, fromViewController: UIViewController, identifier: String?) -> UIStoryboardSegue {
return UIStoryboardSegue(identifier: identifier, source: fromViewController, destination: toViewController) {
let fromView = fromViewController.view
let toView = toViewController.view
if let containerView = fromView.superview {
let initialFrame = fromView.frame
var offscreenRect = initialFrame
var offscreenRectFinal = initialFrame
offscreenRect.origin.x += CGRectGetWidth(initialFrame)
offscreenRectFinal.origin.x -= CGRectGetWidth(initialFrame)
toView.frame = offscreenRect
containerView.addSubview(toView)
let duration: NSTimeInterval = 1.0
let delay: NSTimeInterval = 0.0
let options = UIViewAnimationOptions.CurveEaseInOut
let damping: CGFloat = 0.9
let velocity: CGFloat = 4.0
UIView.animateWithDuration(duration, delay: delay, usingSpringWithDamping: damping,
initialSpringVelocity: velocity, options: options, animations: {
toView.frame = initialFrame
fromView.frame = offscreenRectFinal
}, completion: { finished in
fromView.removeFromSuperview()
if let navController = toViewController.navigationController {
navController.popToViewController(toViewController, animated: false)
}
})
}
}
}
But now I get an error message:
2015-05-12 08:47:31.841 PING0.4[4343:1308313] Warning: Attempt to present <NotificationViewController: 0x177030b0> on <PING0_4.ViewController: 0x16271000> which is already presenting <NotificationViewController: 0x1a488170>
And I am blocked in my app. I can go from VC1 to VC2, then back to VC2 but then I cannot get back to VC1 again. It looks like I can only use this segue once.
Any one has any idea of what is going on?
Created Sample code for unwind segue with above transition animation code. Checkout SampleUnwind project that will help you to understand unwind segue(and how simple it is).
In project there is one navigation controller and inside it there are three view controller (Home->First->second).
In Home controller following unwind action is created, which will be called when 'Home' button of second controller is tapped(simple unwind stuff).
#IBAction func unwindToHomeViewController(segue:UIStoryboardSegue) {
}
I have created TempNavigationController subclassing UINavigationController and set that TempNavigationController to navigation controller in storyboard.
Above method you given is pesent inside it as this will be container of fromViewController as per following referance.
Reference: Apple documentation about Transitioning Between Two Child View Controllers.
You can compare this with your project and maybe you can find any duplicate(or multiple/wrong) segue in your project.

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