Presented view controller disappears after animation using custom UIViewController animations - ios

I've looked around for an answer for this and spent the last two hours pulling my hair out to no end.
I'm implementing a very basic custom view controller transition animation, which simply zooms in on the presenting view controller and grows in the presented view controller. It adds a fade effect (0 to 1 alpha and visa versa).
It works fine when presenting the view controller, however when dismissing, it brings the presenting view controller back in all the way to fill the screen, but then it inexplicably disappears. I'm not doing anything after these animations to alter the alpha or the hidden values, it's pretty much a fresh project. I've been developing iOS applications for 3 years so I suspect this may be a bug, unless someone can find out where I'm going wrong.
class FadeAndGrowAnimationController : NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController!, presentingController presenting: UIViewController!, sourceController source: UIViewController!) -> UIViewControllerAnimatedTransitioning! {
return self
}
func animationControllerForDismissedController(dismissed: UIViewController!) -> UIViewControllerAnimatedTransitioning! {
return self
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {
return 2
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as UIViewController
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as UIViewController
toViewController.view.transform = CGAffineTransformMakeScale(0.5, 0.5)
toViewController.view.alpha = 0
transitionContext.containerView().addSubview(fromViewController.view)
transitionContext.containerView().addSubview(toViewController.view)
transitionContext.containerView().bringSubviewToFront(toViewController.view)
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
fromViewController.view.transform = CGAffineTransformScale(fromViewController.view.transform, 2, 2)
fromViewController.view.alpha = 1
toViewController.view.transform = CGAffineTransformMakeScale(1, 1)
toViewController.view.alpha = 1
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
})
}
}
And the code to present:
let targetViewController = self.storyboard.instantiateViewControllerWithIdentifier("Level1ViewController") as Level1ViewController
let td = FadeAndGrowAnimationController()
targetViewController.transitioningDelegate = td
targetViewController.modalPresentationStyle = .Custom
self.presentViewController(targetViewController, animated: true, completion: nil)
As you can see, a fairly basic animation. Am I missing something here? Like I said, it presents perfectly fine, then dismisses 99.99% perfectly fine, yet the view controller underneath after the dismissal is inexplicably removed. The iPad shows a blank screen - totally black - after this happens.

This seems to be an iOS8 bug. I found a solution but it is ghetto. After the transition when a view should be on-screen but isn't, it needs to be added back to the window like this:
BOOL canceled = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!canceled];
if (!canceled)
{
[[UIApplication sharedApplication].keyWindow addSubview: toViewController.view];
}
You might need to play around with which view you add back to the window, whether to do it in canceled or !canceled, and perhaps making sure to only do it on dismissal and not presentation.
Sources: Container view disappearing on completeTransition:
http://joystate.wordpress.com/2014/09/02/ios8-and-custom-uiviewcontrollers-transitions/

I was having the same problem when dismissing a content view controller. My app has this parent view controller showing a child view controller. then when a subview in the child is tapped, it shows another vc (which I am calling the content vc)
My problem is that, when dismissing the contentVC, it should go to child VC but as soon as my custom transition finishes, childVC suddenly disappears, showing the parent VC instead.
What I did to solve this issue is to
change the .modalPresentationStyle of the childVC presented by parentVC from the default .automatic to .fullscreen.
Then changed the .modalPresentationStyle of contentVC to .fullscreen as well.
This solves the issue. but it won't show your child VC as a card sheet (when using .overCurrentContext or automatic) which is new in iOS 13.

had the same problem ios 8.1
my swift code after completeTransition
if let window = UIApplication.sharedApplication().keyWindow {
if let viewController = window.rootViewController {
window.addSubview(viewController.view)
}
}

Related

How to Push a ViewController with a ModalView?

How can I push a UIViewController (VC) presenting a modal view (MV) to screen with a single left-to-right transition animation?
I have tried:
Setting the modal transition style of MV, and then adding both controllers to the viewControllers of the navigation controller. This however results in MV just being a controller dismissed as any other controller in the stack.
I have tried presenting MV from VC with no animation, then adding VC to the viewControllers and presenting the stack as above. This resulted in MV being instantly presented when the transition to VC starts, while VC itself animates to screen as expected. E.g. the MV doesn't follow VC when it slides in.
I have tried presenting MV from VC with animation enabled, but that results in two transitions: first VC animates to screen, then VC slides up.
I'm out of ideas, yet I would like a native and clean solution: How to transition to VC with a single normal left-to-right push, when VC is entirely covered by MV?
Key point is that MV animates together with VC; MV would appear and behave like a full screen subview, but when dismissed it animates off screen like any other modal view controller.
There are many solutions to this issue. I think UIPresentationController can provide an important clue here.
I try to make the answer as simple as possible here. You may change parameters or even subclass UIPresentationController to achieve a full animation as you wish.
import UIKit
//Green
class TransViewController: UIViewController {
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
let dest = segue.destination
let nextVC = storyboard?.instantiateViewController(withIdentifier: "modelViewController")
if let myPresenter = dest.presentationController{
myPresenter.presentedView!.addSubview(nextVC!.view)
nextVC!.view.center = CGPoint.init(x: nextVC!.view.center.x + nextVC!.view.frame.width , y: nextVC!.view.center.y)
UIView.animate(withDuration: 0.5, animations: {
nextVC!.view.center = self.view.center
}) { (success) in
dest.present(nextVC!, animated: false, completion: nil)
}
}
}
}
//Yellow ; "modelViewController"
class ModelViewController: UIViewController {
#IBAction func click(_ sender : UIButton){
self.dismiss(animated: true, completion: nil)
}
}

UIViewController In-Call Status Bar Issue

Issue:
Modally presented view controller does not move back up after in-call status bar disappears, leaving 20px empty/transparent space at the top.
Normal : No Issues
In-Call : No Issues
After In-Call Disappears:
Leaves a 20px high empty/transparent space at top revealing orange view below. However the status bar is still present over the transparent area. Navigation Bar also leaves space for status bar, its' just 20px too low in placement.
iOS 10 based
Modally presented view controller
Custom Modal Presentation
Main View Controller behind is orange
Not using Autolayout
When rotated to Landscape, 20px In-Call Bar leaves and still leaves 20px gap.
I opt-out showing status bar in landscape orientations. (ie most stock apps)
I tried listening to App Delegates:
willChangeStatusBarFrame
didChangeStatusBarFrame
Also View Controller Based Notifications:
UIApplicationWillChangeStatusBarFrame
UIApplicationDidChangeStatusBarFrame
When I log the frame of presented view for all four above methods, the frame is always at (y: 0) origin.
Update
View Controller Custom Modal Presentation
let storyboard = UIStoryboard(name: "StoryBoard1", bundle: nil)
self.modalVC = storyboard.instantiateViewController(withIdentifier: "My Modal View Controller") as? MyModalViewController
self.modalVC!.transitioningDelegate = self
self.modalVC.modalPresentationStyle = .custom
self.modalVC.modalPresentationCapturesStatusBarAppearance = true;
self.present(self.modalVC!, animated: true, completion: nil)
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
toViewController!.view.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [.curveEaseOut], animations: { () -> Void in
toViewController!.view.transform = CGAffineTransform.identity
}, completion: { (completed) -> Void in
transitionContext.completeTransition(completed)
})
}
I've been looking for a solution for 3 days. I don't like this solution but didn't found better way how to fix it.
I'he got situation when rootViewController view has bigger height for 20 points than window, when I've got notification about status bar height updates I manually setup correct value.
Add method to the AppDelegate.swift
func application(_ application: UIApplication, didChangeStatusBarFrame oldStatusBarFrame: CGRect) {
if let window = application.keyWindow {
window.rootViewController?.view.frame = window.frame
}
}
After that it works as expected (even after orientation changes).
Hope it will help someone, because I spent too much time on this.
P.S. It blinks a little bit, but works.
I faced this problem too but after I put this method, problem is gone.
iOS has its default method willChangeStatusBarFrame for handling status bar. Please put this method and check it .
func application(_ application: UIApplication, willChangeStatusBarFrame newStatusBarFrame: CGRect) {
UIView.animate(withDuration: 0.35, animations: {() -> Void in
let windowFrame: CGRect? = ((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame
if newStatusBarFrame.size.height > 20 {
windowFrame?.origin?.y = newStatusBarFrame.size.height - 20
// old status bar frame is 20
}
else {
windowFrame?.origin?.y = 0.0
}
((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame = windowFrame
})
}
Hope this thing will help you.
Thank you
I had the same issue with the personnal hospot modifying the status bar.
The solution is to register to the system notification for the change of status bar frame, this will allow you to update your layout and should fix any layout issue you might have.
My solution which should work exactly the same for you is this :
In your view controller, in viewWillAppear suscribe to the UIApplicationDidChangeStatusBarFrameNotification
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(myControllerName.handleFrameResize(_:)), name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
Create your selector method
func handleFrameResize(notification: NSNotification) {
self.view.layoutIfNeeded() }
Remove your controller from notification center in viewWillDisappear
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
You also need your modal to be in charge of the status bar so you should set
destVC.modalPresentationCapturesStatusBarAppearance = true
before presenting the view.
You can either implement this on every controller susceptible to have a change on the status bar, or you could make another class which will do it for every controller, like passing self to a method, keep the reference to change the layout and have a method to remove self. You know, in order to reuse code.
I think this is a bug in UIKit. The containerView that contains a presented controller's view which was presented using a custom transition does not seem to move back completely when the status bar returns to normal size. (You can check the view hierarchy after closing the in call status bar)
To solve it you can provide a custom presentation controller when presenting. And then if you don't need the presenting controller's view to remain in the view hierarchy, you can just return true for shouldRemovePresentersView property of the presentation controller, and that's it.
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool {
return true
}
}
or if you need the presenting controller's view to remain, you can observe status bar frame change and manually adjust containerView to be the same size as its superview
class PresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
NotificationCenter.default.addObserver(self,
selector: #selector(self.onStatusBarChanged),
name: .UIApplicationWillChangeStatusBarFrame,
object: nil)
}
#objc func onStatusBarChanged(note: NSNotification) {
//I can't find a way to ask the system for the values of these constants, maybe you can
if UIApplication.shared.statusBarFrame.height <= 20,
let superView = containerView?.superview {
UIView.animate(withDuration: 0.4, animations: {
self.containerView?.frame = superView.bounds
})
}
}
}
I've been looking for a solution to this problem. In fact, I posted a new question similar to this one. Here: How To Avoid iOS Blue Location NavigationBar Messing Up My StatusBar?
Believe me, I've been solving this for a couple of days now and it's really annoying having your screen messed up because of the iOS's status bar changes by in-call, hotspot, and location.
I've tried implementing Modi's answer, I put that piece of code in my AppDelegate and modified it a bit, but no luck. and I believe iOS is doing that automatically so you do not have to implement that by yourself.
Before I discovered the culprit of the problem, I did try every solution in this particular question. No need to implement AppDelegate's method willChangeStatusBar... or add a notification to observe statusBar changes.
I also did redoing some of the flows of my project, by doing some screens programmatically (I'm using storyboards). And I experimented a bit, then inspected my previous and other current projects why they are doing the adjustment properly :)
Bottom line is: I am presenting my main screen with UITabBarController in such a wrong way.
Please always take note of the modalPresentationStyle. I got the idea to check out my code because of Noah's comment.
Sample:
func presentDashboard() {
if let tabBarController = R.storyboard.root.baseTabBarController() {
tabBarController.selectedIndex = 1
tabBarController.modalPresentationStyle = .fullScreen
tabBarController.modalTransitionStyle = .crossDissolve
self.baseTabBarController = tabBarController
self.navigationController?.present(tabBarController, animated: true, completion: nil)
}
}
I solve this issue by using one line of code
In Objective C
tabBar.autoresizingMask = (UIViewAutoResizingFlexibleWidth | UIViewAutoResizingFlexibleTopMargin);
In Swift
self.tabBarController?.tabBar.autoresizingMask =
UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleTopMargin.rawValue)))`
You just need to make autoresizingMask of tabBar flexible from top.
In my case, I'm using custom presentation style for my ViewController.
The problem is that the Y position is not calculated well.
Let's say the original screen height is 736p.
Try printing the view.frame.origin.y and view.frame.height, you'll find that the height is 716p and the y is 20.
But the display height is 736 - 20(in-call status bar extra height) - 20(y position).
That is why our view is cut from the bottom of the ViewController and why there's a 20p margin to the top.
But if you go back to see the navigation controller's frame value.
You'll find that no matter the in-call status bar is showing or not, the y position is always 0.
So, all we have to do is to set the y position to zero.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let f = self.view.frame
if f.origin.y != 0 {
self.view.frame = CGRect(x: f.origin.x, y: 0, width: f.width, height: f.height)
self.view.layoutIfNeeded()
self.view.updateConstraintsIfNeeded()
}
}
Be sure to set the frame of the view controller's view you are presenting to the bounds of the container view, after it has been added to the container view. This solved the issue for me.
containerView.addSubview(toViewController.view)
toViewController.view.frame = containerView.bounds

Changing rootViewController for Nav causes UISplitViewController to show detail on Compact portrait orientation

I'm running into an issue where after changing the rootViewController on my UINavigationController and changing it back to my original UINavigationController, a UISplitViewController begins to show both it's master and detail view in a phone device on compact/portrait orientation (so not only on plus size phones, but also others).
Basic overview of architecture:
A TabBarController houses several tabs. One of these tabs is a UISplitViewController. I currently override the following to ensure that the MasterViewController is shown on compact orientations:
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
// this prevents phone from going straight to detail on showing the split view controller
return true
}
This works fine and displays the master on portrait as expected. At any point pressing a button on another tab can create a new UINavigationController instance and display it, in which I'm doing the below to change the rootViewController to the newly created UINavigationController to display:
let appDelegate = UIApplication.shared.delegate
appDelegate?.window??.rootViewController = newNavVC
On dismiss, I'm just swapping the UINavigationController back to the original one through the same code above. However, once I do this one time (create nav/display/dismiss), and I switch my tab back to the one with the UISplitViewController, it changes itself to show a side-by-side master detail view. I didn't know this was possible in portrait mode for compact sizing. I tried changing to any of the 4 preferred display modes in the UISplitViewController, but that didn't fix it.
Below is what it looks like (iPhone 6 simulator), am I missing delegates or misunderstanding collapsing?
Before:
After:
You can replace the the logic that assigned the rootViewController with the code snippet found at this link:
Leaking views when changing rootViewController inside transitionWithView
Basically you just create an extension for the UIWindow class that will set the root view controller correctly.
extension UIWindow {
/// Fix for https://stackoverflow.com/a/27153956/849645
func set(rootViewController newRootViewController: UIViewController, withTransition transition: CATransition? = nil) {
let previousViewController = rootViewController
if let transition = transition {
// Add the transition
layer.add(transition, forKey: kCATransition)
}
rootViewController = newRootViewController
// Update status bar appearance using the new view controllers appearance - animate if needed
if UIView.areAnimationsEnabled {
UIView.animate(withDuration: CATransaction.animationDuration()) {
newRootViewController.setNeedsStatusBarAppearanceUpdate()
}
} else {
newRootViewController.setNeedsStatusBarAppearanceUpdate()
}
/// The presenting view controllers view doesn't get removed from the window as its currently transistioning and presenting a view controller
if let transitionViewClass = NSClassFromString("UITransitionView") {
for subview in subviews where subview.isKind(of: transitionViewClass) {
subview.removeFromSuperview()
}
}
if let previousViewController = previousViewController {
// Allow the view controller to be deallocated
previousViewController.dismiss(animated: false) {
// Remove the root view in case its still showing
previousViewController.view.removeFromSuperview()
}
}
}

Custom flip animation segue - Swift 3

My goal is to perform a flip-animation from view1 to view2 and vice versa.
I only want to flip the views not the whole viewController
Set up as follows:
i have a view controller (fromViewController) in my storyboard embedded in a navigationController. I drew a segue to a second view controller (toViewController) and made it a custom segue.
Now i want to add a flip animation to flip the views of the view controllers from left to right and right to left.
The forward animation already works (from fromViewController to toViewController) but he backward animation does not work.
I checked a lot of similar questions here but i couldn't find an answer that fit my needs.
I hope someone can help me out of this.
The class for the custom segue is FlipSegue.
here is my code:
import UIKit
extension UIViewController{
var isVisible: Bool{
return self.isViewLoaded && self.view.window != nil
}
}
class FlipSegue: UIStoryboardSegue {
override func perform() {
let fromViewController = self.source
let toViewController = self.destination
if fromViewController.isVisible{
UIView.transition(from: fromViewController.view, to: toViewController.view, duration: 1, options: .transitionFlipFromLeft, completion: nil)
}
else{
UIView.transition(from: toViewController.view, to: fromViewController.view, duration: 1, options: .transitionFlipFromRight, completion: nil)
}
}
}
FYI: The toViewController is not part of the NavigationController.
EDIT: I perform the segue by tapping on a UIBarButton on the left side of the navigation controller. The Button is calling the perform function in the FlipSegue class. Maybe the problem is that the toViewController is not part of the navigation stack? So i added it to the navigation stack and then i got a back button and i could return to the fromViewController with the standard animation, but this is not what i wanted.
I haven't tried this, but you should probably use from:fromViewcontroller.view in both cases, since in a back segue, from is still the starting VC.

How to avoid animateTransition(transitionContext) protocol method(in navigationController push methods) from removing previous ViewController's view?

i have to push a viewController that has a dimming view with an alpha of 0.5. Because of this, the ViewController's view has to show the previous controller's view behind this dimming background. The problem is that i'm using a navigationController that uses a UIViewControllerAnimatedTransitioning protocol to customize the animation. By default, after pushing the new viewController onto the stack, the navigationController automatically removes the previous view. So, how to keep the previous view after completing this transition, is this possible?
NOTES: i don't want to just add the controller's view to the navigationController(This gave me strange behaviors in the navigation functionality), and i do really need to push it in this way, so i can continue using the application code pattern.
CODE:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if isPresenting {
let ContainerView = transitionContext.containerView()
if let PresentedController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) {
if let PresentedView = transitionContext.viewForKey(UITransitionContextToViewKey) {
PresentedView.alpha = 0
PresentedView.frame = transitionContext.finalFrameForViewController(PresentedController)
ContainerView.addSubview(PresentedView)
// i've also tried to add the fromView in the containerView.
UIView.animateWithDuration(0.4, animations: {
PresentedView.alpha = 1
}) {
Completion in
transitionContext.completeTransition(Completion)
}
}
}
} else {
// dismiss code...
}
}
Thanks for your patience.
I had a similar situation a few weeks ago. In my case, I was presenting my view controller modally. The way I got around the problem was to take a snapshot of the fromViewController's view and use it as background for toViewController's view.
To capture snapshot:
UIImage *snapshotImage = nil;
UIGraphicsBeginImageContextWithOptions(self.tabBarController.view.frame.size, NO, 0.0);
if ([self.tabBarController.view drawViewHierarchyInRect:self.tabBarController.view.bounds afterScreenUpdates:YES]) {
snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
You can capture transitionContext.containerView for this. Pass this image to your new VC and set it as the background to that VC's view as:
self.view.backgroundColor = [UIColor colorWithPatternImage:self.bgImage];
P.S. I am not fluent in Swift so you will have to convert the code to swift yourself but hopefully this gives you a decent starting point

Resources