Custom presented UIViewController changing to fullscreen - ios

I have a view controller presented using UIModalPresentationCustom presentation style. I use a custom UIViewControllerTransitioningDelegate to present the view controller as a sidebar (so it slides in from the edge of the screen and does not occupy the full screen).
However when I then present another view controller from this one using UIModalPresentationFullScreen — and then dismiss the full screen view controller, my underlying custom presented controller is suddenly resized to occupy the full screen. Does anyone know why this is the case?
Edit: this is essentially my animateTransition method for presenting the sidebar — I've stripped out most of the code to make it readable. Basically it gets the container from the transitionContext, adds and animates the destination view controller's view to the container.
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIView *container = transitionContext.containerView;
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
if( toVC.isBeingPresented )
{
[container addSubview:toView];
//... Animate some new frame for toView
//Call [transitionContext completeTransition:YES] on animation completion
}
else
{
//... Animate fromView out
//On completion remove fromView from superview
//Call [transitionContext completeTransition:YES] on animation completion
}
}
Edit 2: Doing a little more research, I notice that the frame of my custom presented view controller's view is being set when the view controller above it in the modal stack is dismissed. The following stack trace leads to the frame being set as full screen:
0 -[MyCustomPresentedViewControllerView setFrame:]
1 -[UIView(MPAdditions) setFrameOrigin:]
2 -[UIViewControllerAccessibility(SafeCategory) dismissViewControllerWithTransition:completion:]
3 -[UIViewController dismissViewControllerAnimated:completion:]

Changing the presentation style by assigning:
viewController.modalPresentationStyle = UIModalPresentationOverCurrentContext;
results in the modal presentation controller occupying a fraction of the screen, the same fraction as the original presented view controller of the UIPresentationController.
However, using viewController.modalPresentationStyle = UIModalPresentationOverFullScreen
is better for this case, since his modal controller is full screen.

I am experiencing the same issue. I've tried debugging it using symbolic breakpoints and there seem to be some internal call on some kind of layout manager that does this.
While I wasn't able to "solve" this (it seems to me like a bug in the SDK), I was able to come up with a workaround that fixes this. Basically, you have to set the correct dimensions of the presented view at two opportune times. Like this:
In the view controller that was presented using your custom UIPresentationController, add this:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
dispatch_async(dispatch_get_main_queue(), ^{
self.view.frame = [self.presentationController frameOfPresentedViewInContainerView];
});
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.view.frame = [self.presentationController frameOfPresentedViewInContainerView];
}
And if you are wondering: yes, you do need it to do in two places. Because weirdly enough, when I did it only in the viewDidAppear async block, it got broken (resized to fullscreen) once the animation finished.

Try to use:
viewController.modalPresentationStyle = UIModalPresentationOverCurrentContext;
It helped me in iOS 8.1

Related

iOS 8 bug with dismissViewControllerAnimated: completion: animation?

iOS documentation for dismissViewControllerAnimated:completion: states:
If you present several view controllers in succession, thus building a
stack of presented view controllers, calling this method on a view
controller lower in the stack dismisses its immediate child view
controller and all view controllers above that child on the stack.
When this happens, only the top-most view is dismissed in an animated
fashion; any intermediate view controllers are simply removed from the
stack. The top-most view is dismissed using its modal transition
style, which may differ from the styles used by other view controllers
lower in the stack.
This means when dismissing two modal view controllers at once using
[[[self presentingViewController] presentingViewController] dismissViewControllerAnimated:YES completion:nil];
the animation shown should be the top modal view being dismissed.
This is indeed the case in iOS 7 and prior, but in iOS 8 the animation shown is not the top-most view (in my experience, it's the second top-most view). Is this behavior a bug in iOS 8 or am I doing something wrong?
As commented above: I see the exact same issue in an unwind segue context. I just toke the workaround as described here using a screenshot and add it as a subview to all intermediate viewControllers: How to dismiss a stack of modal view controllers with animation without flashing on screen any of the presented VCs between the top and bottom?
// this in during unwind in a custom UIStoryboardSegue (that is the reason why it might look wrong with what is what: srcViewController and destViewController
UIViewController* aPresentedViewController = destViewController.presentedViewController;
while (aPresentedViewController != nil) {
if (aPresentedViewController == srcViewController) {
break;
}
UIView *anotherSrcViewCopy = [srcViewController.view snapshotViewAfterScreenUpdates: NO];
anotherSrcViewCopy.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[aPresentedViewController.view addSubview:anotherSrcViewCopy];
// recurse through the presentedViewController hierarchy
aPresentedViewController = aPresentedViewController.presentedViewController;
}
Same problem and same solution here than #theguy.
Here's my version in Swift without iterating on all the view controllers :
guard
let presentedViewController = segue.destination.presentedViewController,
let viewToCopy = segue.source.view.snapshotView(afterScreenUpdates: false)
else { return }
viewToCopy.autoresizingMask = [.flexibleWidth, .flexibleHeight]
presentedViewController.view.addSubview(viewToCopy)

Creating custom push segue

I'm writing a custom segue class (HySegue) which allows views to animate the transition. The code is working great, except when under a UINavigationController stack. Actually, the transitions run and animate well, but the top UINavigationBar is what is causing me problems.
When first animating, I add the destination view as a subview of the source view. As such:
UIView * sourceView = sourceViewController.view;
UIView * destinationView = viewControllerToPresent.view;
// Force the source view to layout
[sourceView addSubview:destinationView];
[sourceView layoutIfNeeded];
When the transition is over, I present the destination view controller:
UIViewController * parentViewController = viewControllerToDismiss.parentViewController;
UIView * destinationView = destinationViewController.view;
// Break the view hierarchy that was setup earlier
[destinationView removeFromSuperview];
// When presenting in a UINavigationController stack, push the destination view controller
if ([parentViewController isKindOfClass:[UINavigationController class]]) {
[(UINavigationController *)parentViewController pushViewController:destinationViewController
animated:NO];
}
else {
// Present the destination view controller
[viewControllerToDismiss presentViewController:destinationViewController
animated:NO
completion:nil];
[viewControllerToDismiss willMoveToParentViewController:nil];
[viewControllerToDismiss.view removeFromSuperview];
[viewControllerToDismiss removeFromParentViewController];
[viewControllerToDismiss didMoveToParentViewController:nil];
}
The problem is that during the animation the destination view does not know that it's being pushed in a navigation stack and so the top bar, although visible, is not taken into account in the view's bounds. That is, the navigation bar is visible because it's visible for the source view controller, but the destination view controller knows nothing about it. When the animation finishes the destination view controller is pushed onto the stack, so it now knows about the navigation bar, and all my content with Top Space to Top Layout Guides constraints jumps down on the frame.
The top bar also doesn't show in IB. This is when I use my custom segue:
And this is when I use show segue:
Notice that the bar shows on the later but not on the former.
What I was wondering is how IB knows that it's a push segue. Is it some flag? Is it because of the specific class it's using? How would I make my custom segue also a push segue?
Edit: I loved the segue's type in prepareForSegue:sender: and it seems its type is UIStoryboardPushSegue, which is not a public or documented class. How can I can I solve it then? I already tried setting the destination view's frame and bounds from the source view.

Animating the presenting view in a UIPresentationController

For some context, I recommend reading this:
Very relevant question: "From View Controller" disappears using UIViewControllerContextTransitioning
Very relevant answer: https://stackoverflow.com/a/25901154/751268
I'm trying to implement a custom view controller transition that animates the new view controller to cover half the screen, while simultaneously shrinking the presenting view controller to 90% (centered in the window, underneath the presented view controller).
First, my problem was that viewFromKey: returned nil. To solve that, the answer mentioned:
If you want to animate the presenting view controllers's view you should consider using UIModalPresentationFullscreen style or continue using UIModalPresentationCustom and implement your own subclass of UIPresentationController with shouldRemovePresentersView returning YES.
I did that, and viewFromKey: doesn't return nil anymore, but now the presenting view controller disappears completely (which makes sense considering I explicitly say it should by implementing shouldRemovePresentersView).
I add the presenting view controller's view to the container view, but it still gets removed. Is there anything else I should be doing to get this working?
Here's some relevant code:
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
BOOL show = self.isPresentation;
UIView *menuView = show ? toView : fromView;
UIView *backView = show ? fromView : toView;
UIView *containerView = [transitionContext containerView];
[containerView addSubview:backView];
[containerView addSubview:dimmedView];
[containerView addSubview:menuView];
// Adjust transforms, alpha and perform animations
I thought that by returning YES from shouldRemovePresentersView and manually adding it to the containerView, that should fix the issue, but backView gets removed anyway...
I'm adding another answer, as my response is too long to fit in a comment.
First of all the viewForKey is available in iOS8, so unless you are targeting iOS8 only (why?) you should not use it, or use it after checking that the UIViewControllerContextTransitioning responds to that selector and use the viewControllerForKey for iOS7.
With that being said, it seems to me that this is a bug and I explain my self:
If you look at the UIPresentationController header file, you will see that it says
// Indicate whether the view controller's view we are transitioning from will be removed from the window in the end of the
// presentation transition
// (Default: YES)
- (BOOL)shouldRemovePresentersView;
So as you see the default is YES, so this should only overriden when you specifically want to say NO.
However, you are right, without this being set explicitly to YES, the viewForKey for the UITransitionContextFromViewControllerKey remains nil.
I think you should fill in a bug report for this and for now use the viewControllerForKey which is fine to be used (nothing wrong with this) as it's not deprecated and works in both OS versions without a problem.
And the reason this is most likely a bug is that the viewForKey should return a view for theUITransitionContextFromViewControllerKey when the shouldRemovePresentersView is explicitly set to NO and not YES.
My 2 cents
Why are you setting shouldRemovePresentersView to YES if what you want is that the fromView remains visible?
I had the same problem as you, as in my custom 3d presentation the parent view controller was removed as soon as the transition was completed.
The solution there is to change the modalPresentationStyle to UIModalPresentationOverCurrentContext
This will specifically prohibit the system to remove the owner viewController when the transition ends.
Still my 3D transition suffered from animation issues, when using this approach.
Si I ended up using a UIModalPresentationCustom modalPresentationStyle which solved almost all my issues, with the exception that the new view controller (which was a UINavigationController) would not move down when the in-call status bar would appear.
To solve this, I ended up changing the modalPresentationStyle back to UIModalPresentationFullScreen after the transition had been completed.
Here is my code that shows the new viewController with the custom presentation:
//show Login Screen
LoginViewController *viewController = [[LoginViewController alloc] initWithNibName:#"LoginViewController" bundle:nil];
UINavigationController *loginNavController = [[UINavigationController alloc] initWithRootViewController:viewController];
loginNavController.interactivePopGestureRecognizer.enabled = YES;
loginNavController.transitioningDelegate = self.customTransitionDelegate;
loginNavController.modalPresentationStyle = UIModalPresentationCustom;
[navigationController presentViewController:loginNavController animated:YES
completion:^{
//important, else status bar is not moving entire screen down....
loginNavController.modalPresentationStyle = UIModalPresentationFullScreen;
}
];
Also very important is that you must NOT add the old view to the containerView when you run your dismissal animation
So if the animation is the presenting, add your toView to the containerView
UIView* inView = [transitionContext containerView];
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
.....
......
[inView insertSubview:toViewController.view aboveSubview:fromViewController.view];
But on the dismissal presentation DO NOT ADD the view to the containerView as it is still showing (since we specifically asked the system NOT to remove it), but simply animate between the two views.

In iOS 7, with customer VC transitioning, is it possible to make "fromviewcontroller" on screen after presentViewController?

Many apps have the feature which, user can pull down one view, and another view shows up from underneath it. But the first view is still visible on the bottom. (e.g. Facebook Paper App).
If the other view is from another view controller, is it possible to achieve this by using iOS 7's custom view controller transitioning API ?
In my test, it is possible to do the "presenting" part, but the "dismissing" part has a glitch. Whenever we call dismissViewController, the "toViewController" takes over the full screen even before calling the transitioningDelegate methods.
Anyone more familiar with this ? Thanks !
If the other view is from another view controller, is it possible to achieve this by using iOS 7's custom view controller transitioning API ?
Yes, it's possible. You can use the snapshot API to take a snapshot of any UIView. With this you can take a snapshot of view controller you're transitioning from, then add it to the containerView below the view controller you're transitioning to.
For example, in the -animateTransition: method of your class that adopts UIViewControllerAnimatedTransitioning take a snapshot of the view controller you're transitioning from and add it as a subview beneath the view controller you're transitioning to:
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
UIView * containerView = transitionContext.containerView;
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// take snapshot of from view controller
UIView * fromSnapshotView = [fromViewController.view snapshotViewAfterScreenUpdates:NO];
[containerView insertSubview:fromSnapshotView belowSubview:toViewController.view];
// Then do your animations on the to view controller to animate it into view as well as the fromSnapshotView
// Finally, don't forget to call:
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}
In my test, it is possible to do the "presenting" part, but the "dismissing" part has a glitch. Whenever we call dismissViewController, the "toViewController" takes over the full screen even before calling the transitioningDelegate methods.
If your call to dismiss doesn't fire your transitioning delegate methods make sure that you set the transitioning delegate on that view controller before dismissing.

iOS - Semi-transparent modal view controller

I want to present a view controller with a slightly transparent background modally over the current view, such that the first view is slightly visible under the modal view.
I set the alpha value of the modal view controller and set the modalPresentationStyle to UIModalPresentationCurrentContext, as suggested in another post.
The result is that the view background is transparent when animating up, but when view controller is in place it changes to opaque black. It goes back to being transparent while animating the dismissal.
How can I get it to be transparent when active ?
I have tested in iOS 6 and 7. The code I am using follows:
MyModalViewController *viewController = [[MyModalViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController];
[navController setNavigationBarHidden:YES];
self.navigationController.modalPresentationStyle = UIModalPresentationCurrentContext;
[self.navigationController presentViewController:navController animated:YES completion:NULL];
iOS 8 added a new modal presentation style specifically for this purpose:
presentedViewController.modalPresentationStyle = UIModalPresentationOverFullScreen
From the spec:
UIModalPresentationOverFullScreen
A view presentation style in which the presented view covers the screen. The views beneath the presented content are not removed from the view hierarchy when the presentation finishes. So if the presented view controller does not fill the screen with opaque content, the underlying content shows through.
If you are targeting ios 8 and above you can set the modal presentation style to "over current context" and you are done.
If ios 7 and below, you would have to create a custom transition style so that the presenting screen doesn't go blank after transition. That is rather complicated.
The solution I present offers a lot of flexibility: make a screenshot before showing the modal dialog and set that as the background image for the application window. By default, that background is black (that is what you see when the back view controller dissapears). Change the background to the screenshot of the app. Make the screenshot in the viewWillAppear or viewDidLoad method of your transparent view. This works even with push segues, not only modal dialogs, but you should avoid animations. In general, avoid animations which affect the position of the background view because those will make it seem like it snaps back into place when transition finishes. It is a good idea to reset the background to its previous black image on viewDidDissapear to avoid unwanted effects.
You can maintain a stack of such background images and you can do multiple "transparent" push seques. Or have some complex/deep menu which appears on top of some main screen. For these many reasons I think this solution is better than rolling your own transitioning code. It is more flexible and easier to implement, and you don't have to deal with the animations yourself.
The reason that the BG view controllers disappear after a modal is shown is that the default transition in iOS 7 removes the BG view after animation completed. If you define your own transition and you set your BG view not to be removed (just changing its alpha) then you will have the transparent modal view.
Same problem occured to me. I have solved it by looking at the following url about a custom alert controller. I managed to get it working even with a UINavigationController.
Swift
let viewController = UIViewController()
viewController.providesPresentationContextTransitionStyle = true
viewController.definesPresentationContext = true
viewController.modalPresentationStyle = .overCurrentContext
viewController.modalTransitionStyle = .crossDissolve
DispatchQueue.main.async {
self.navigationController?.present(viewController, animated: true, completion: nil)
}
Objective C
UIViewController *viewController = [UIViewController new];
viewController.providesPresentationContextTransitionStyle = true;
viewController.definesPresentationContext = true;
viewController.modalPresentationStyle = UIModalPresentationOverCurrentContext;
viewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController presentViewController:viewController animated:true completion:nil];
});
Here is a solution.
Create your presenting view controller. Add a backView to this view controller's main view. Name this as backView.
In SecondViewController.m
-(void)viewDidLoad
{
// Make the main view's background clear, the second view's background transparent.
self.view.backgroundColor = [UIColor clearColor];
UIView* backView = [[UIView alloc] initWithFrame:self.view.frame];
backView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.6];
[self.view addSubview:backView];
}
Now you have a view controller with half transparent background. You can add anything you want to the self.view , the rest will be half transparent.
After that, in FirstViewController.m
self.modalPresentationStyle = UIModalPresentationCurrentContext;
[self presentViewController:secondViewController animated:YES completion:nil];
My solution is this:
Create a custom transparent overlay UIView that comes over any view, navigationbar and tabbbar.
-In the navigation controller (or tabbar controller) that your view controller is embedded in I create a custom view with it's frame equal to the frame of the navigation controller's view.
-Then I set it offscreen by setting it's origin.y to navigationController.view.height
-Then I create 2 functions -(void)showOverlay and -(void)hideOverlay that animate the overlay view on and off screen:
- (void)hideOverlay{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.3];
CGRect frm = self.helpView.frame;//helpView is my overlay
frm.origin.y = self.offscreenOffset; //this is an Y offscreen usually self.view.height
self.helpView.frame = frm;
[UIView commitAnimations];
}
- (void)showOverlay{
[self.view bringSubviewToFront:self.helpView];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.3];
CGRect frm = self.helpView.frame;
frm.origin.y = self.onscreenOffset;
self.helpView.frame = frm;
[UIView commitAnimations];
}
-In my view controller I can just call
[(MyCustomNavCtrl *)self.navigationController showOverlay];
[(MyCustomNavCtrl *)self.navigationController hideOverlay];
And that's about it.
FYI: The syntax is now:
childVC.modalPresentationStyle = UIModalPresentationStyle.OverFullScreen
Why don't you try setting this in AppDelegate
self.window.rootViewController.modalPresentationStyle = UIModalPresentationCurrentContext;
then changing the alpha on the view being presented

Resources