Navigation Bar jumping when presenting view controller via UIPresentationController subclass - ios

I'm building an iOS 8 app and using UIPresentationController to present a view controller in a custom way. (see my previous question about this here: Replicating the style of the iOS Mail App's Compose Function).
The issue I'm having is that when I present the controller, the navigation bar starts off as 64 points tall and then jumps/shrinks back to 44 once its presentation is finished. My guess is that the view controller realizes it is not covering the status bar and so it shrinks itself down once it comes to its final resting position. I'd like for the navigation bar to be 44 points tall the entire time and not jump/shrink.
The image below is what the view controller looks like at the end of the presentation. It is also what I want it to look like the entire time. Any thoughts on how to keep the navigation bar at 44 points the entire time?
UPDATE (3/24/2015):
I referenced a blog post from a while back to find some more information on this issue. Basically, UINavigationController draws its navigation bar either 64 or 44 points tall depending on if its view's frame is matched up with the app's window or not. So I need some way of telling the navigation controller that its final resting position will not be lined up with the window, and that the nav bar should be drawn 44 points tall.
http://blog.jaredsinclair.com/post/61507315630/wrestling-with-status-bars-and-navigation-bars-on

Finally found an answer to this question. It's explained in this previous stack overflow post:
Navigation bar gets adjusted after calling completeTransition: in custom transition
Thank you for not making me use my hard earned rep to start a bounty!

I had an issue a bit like yours, where the navigation bar would resize after [transitionContext completeTransition:YES] was called, based on visual contiguity of the navigationBar's frame sharing a border with the UIWindow's top. My navigation bar was nowhere near the top, so it resized itself to 44px instead of the normal "extend-under-the-status-bar" 64px. To get around this, I simply completed the transition before I animated my toViewController's alpha and position. That is, once everything was positioned properly to be animated in, I called completeTransition: to let the navigationController adjust itself while invisible. So far, this hasn't had any unintended side-effects, and the additional alpha in, move frame animations still continue after you completeTransition.
Here is my animateTransition: method in my presentation animator class that conforms to <UIViewControllerAnimatedTransitioning>
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *presentedViewController = self.presenting ? toViewController : fromViewController;
UIView *containerView = [transitionContext containerView];
NSTimeInterval animationDuration = [self transitionDuration:transitionContext];
if (self.presenting) {
containerView.alpha = 0.0;
presentedViewController.view.alpha = 0.0;
[containerView addSubview:presentedViewController.view];
[UIView animateWithDuration:animationDuration delay:0 options:kNilOptions animations:^{
containerView.alpha = 1.0;
} completion:^(BOOL finished) {
presentedViewController.view.frameTop += 20;
//I complete the transition here, while my controller's view is still invisible,
// but everything is in its proper place. This effectively positions everything
// for animation, while also letting the navigation bar resize itself without jarring visuals.
[transitionContext completeTransition:YES];
//But we're not done quite yet...
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
presentedViewController.view.frameTop -= 20;
presentedViewController.view.alpha = 1.0;
} completion:nil];
}];
}
if (!self.presenting) {
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
presentedViewController.view.alpha = 0.0;
presentedViewController.view.frameTop += 20;
} completion:^(BOOL finished) {
[UIView animateWithDuration:animationDuration delay:0 options:kNilOptions animations:^{
containerView.alpha = 0.0;
} completion:^(BOOL done) {
[transitionContext completeTransition:YES];
}];
}];
}
Hope this helps anyone that finds themselves in my position!

Related

How To Animate UIView Frame with ViewWillLayoutSubview

I have created two UIViews Programmatically and set its frame in ViewWillLayoutSubview function, Now I want to create a swapping animation so that each view swap each others position with animation on a click. For getting this animation I have to interchange its frame inside an animation but at the same time ViewWillLayoutSubview get called which sets frame to initial position.
How can I get my UIViews to be swapped with animation using ViewWillLayoutSubview?
I am not using any type of constraint or xib or storyboard. Whole screen is designed programatically.
Thanks in advance.
-(void)viewWillLayoutSubviews
{
if(Once)
{
// create 2 views here
once = NO;
}
}
put this code in button click
CGRect view2Frame = self.view2.frame;
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
//code with animation
self.view2.frame = self.view1.frame;
self.view1.frame = view2Frame;
} completion:^(BOOL finished) {
//code for completion
}];

How do I get the frame of a subview in the ToViewController during animateTransition:?

I an using the new custom transitions introduced with iOS 7 and I'm trying to move a view from my 'master' view controller to a view in my 'detail' view controller kind of like how Photos.app zooms in on a photo when you tap on it except this view in the detail vc only takes up half of the screen. To do this I'm trying to animate this view to it's final frame by getting the final frame from the detail or toViewController. However the frame I get is the starting frame from Interface Builder for the toViewController's view and not the view that appears after AutoLayer has taken a pass at it. How do I get the frame after it has been set by Auto Layout? I have tried [self.view layoutSubviews] right before the toViewController special view frame is returned to animateTransition: but that still gives the IB layout.
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.alpha = 0;
toViewController.view.alpha = 1;
fromViewController.specialView = [toViewController detailSpecialViewFrame];
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
Call layoutIfNeeded, not layoutSubviews, to force a layout pass. Then the frame should be updated.

UIViewController – issue with custom dismiss transition

Summary
I have a content UIViewController that presents a settings UIViewController using a custom transition. The presentation is with presentViewController:animated:completion:.
When I later dismiss the settings with dismissViewControllerAnimated:completion:, the presenting controller is suddenly jumped back to it's initial position prior to the settings controller presentation.
I have a work around for this on the device but not the simulator. However, I'd like to know what I'm doing wrong rather than hack in a bodge that makes it go away. I also plan to make this animation interactive, and I suspect this issues will amplify when I do this.
Custom Transition – Opening the hood
The desired effect is that the presenting controller slides down the screen, and the presented controller is seen to be lying behind it from where it lifts up to fill the screen. The top of the presenting controller remains on-screen during the lifetime of use of the presented controller. It stays at the bottom of the screen, but above the presented controller.
You could imagine lifting the bonnet on a car (the front presenting controller) to see the engine behind (the presented settings), but the bonnet stays visible at the bottom for a bit of context.
I plan to refine this so that the presenting controller really appears to lift up with perspective in a 3d way, but I've not got that far, yet.
When the settings are dismissed, the original presenting controller (bonnet) should slide back up the screen and the presented controller (settings) sink back slightly (closing the bonnet).
Code
Here's the method that toggles the settings on and off the screen (it's just called by a UIButton). You'll notice that the presenting view controller sets itself up as the <UIViewControllerTransitioningDelegate>.
-(void) toggleSettingsViewController
{
const BOOL settingsAreShowing = [self presentedViewController] != nil;
if(!settingsAreShowing)
{
UIViewController *const settingsController = [[self storyboard] instantiateViewControllerWithIdentifier: #"STSettingsViewController"];
[settingsController setTransitioningDelegate: self];
[settingsController setModalPresentationStyle: UIModalPresentationCustom];
[self presentViewController: settingsController animated: YES completion: nil];
}
else
{
[self dismissViewControllerAnimated: YES completion: nil];
}
}
To implement <UIViewControllerAnimatedTransitioning> the presenting view controller also just returns itself as the <UIViewControllerAnimatedTransitioning>
-(id<UIViewControllerAnimatedTransitioning>) animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
return self;
}
-(id<UIViewControllerAnimatedTransitioning>) animationControllerForDismissedController:(UIViewController *)dismissed
{
// Test Point 1.
return self;
}
So finally, the presenting view controller will receive animateTransition::
-(void) animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *const fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *const toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
const BOOL isUnwinding = [toController presentedViewController] == fromController;
const BOOL isPresenting = !isUnwinding;
UIViewController * presentingController = isPresenting ? fromController : toController;
UIViewController * presentedController = isPresenting ? toController : fromController;
if(isPresenting)
{
// Add the presented controller (settings) to the view hierarchy _behind_ the presenting controller.
[[transitionContext containerView] insertSubview: [presentedController view] belowSubview: [presentingController view]];
// Set up the initial position of the presented settings controller. Scale it down so it seems in the distance. Alpha it down so it is dark and shadowed.
presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
presentedController.view.alpha = 0.7;
[UIView animateWithDuration: [self transitionDuration: transitionContext] animations:^{
// Lift up the presented controller.
presentedController.view.transform = CGAffineTransformMakeScale(1.0, 1.0);
// Brighten the presented controller (out of shadow).
presentedController.view.alpha = 1;
// Push the presenting controller down the screen – 3d effect to be added later.
presentingController.view.layer.transform = CATransform3DMakeTranslation(0,400,0);
} completion: ^(BOOL finished){
[transitionContext completeTransition: ![transitionContext transitionWasCancelled]];
}];
}
else
{
// Test Point 2.
// !!!This line should not be needed!!!
// It resets the presenting controller to where it ought to be anyway.
presentingController.view.layer.transform = CATransform3DMakeTranslation(0,400,0);
[UIView animateWithDuration: [self transitionDuration: transitionContext] animations:^{
// Bring the presenting controller back to its original position.
presentingController.view.layer.transform = CATransform3DIdentity;
// Lower the presented controller again and put it back in to shade.
presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
presentedController.view.alpha = 0.4;
} completion:^(BOOL finished) {
[transitionContext completeTransition: ![transitionContext transitionWasCancelled]];
}];
}
}
-(NSTimeInterval) transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
Problem
In the code above, I've indicated !!!This line should not be needed!!!.
What's happening is that between Test Point 1 and Test Point 2 the screen position of the presenting view controller is reset to be the default full screen bounds. So, instead of being at the bottom of the screen ready to animate back up again smoothly, it suddenly jumps up the screen to position that it is meant to smoothly animate too!
I've tried various approaches to animating the presenting view controller down the screen:
I've changed its view's frame.
I've changed its view's transform.
I've changed its view's layer's 3d transform.
In all cases, at Test Point 1, when the transition delegate is asked for, the presenting controller is set up as I would expect. However, in all cases, at Test Point 2, the presenting view controller has lost the correct position and has been "cleared" to have the normal full screen position that I want to animate it to.
In the work around above I explicitly relocate the presenting view controller back to where it should be at the start of the animation with !!!This line should not be needed!!!. This seems to work on the device with the current version of iOS 7. However, on the simulator, the controller is visible at the cleared position for at least one frame.
I am suspicious that I am doing something else wrong, and that I'm going to get in to trouble with my workaround just masking another problem.
Any ideas what's going on? Thanks!
A few potential gotchas with dismissal of modally presented view controllers using custom transition animations:
Add the presented ("to") view to the container, then bring the presented view front. Don't add the presenting view as you might remove it from its current superview.
On dismiss, UIKit sets the alpha of the presented view to 0 before animateTransition is called. So you'll want to set it to 1.0 or whatever it was at the completion of the present before firing off your dismiss animation(s).
Likewise for the presented view's transform. On dismiss it gets reset to identity before your animateTransition is called.
Given all that, I think this should work:
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = transitionContext.containerView;
const BOOL isUnwinding = [toController presentedViewController] == fromController;
const BOOL isPresenting = !isUnwinding;
UIViewController *presentingController = isPresenting ? fromController : toController;
UIViewController *presentedController = isPresenting ? toController : fromController;
[containerView addSubview:presentingController.view];
[containerView bringSubviewToFront:presentingController.view];
if(isPresenting)
{
// Set up the initial position of the presented settings controller. Scale it down so it seems in the distance. Alpha it down so it is dark and shadowed.
presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
presentedController.view.alpha = 0.7;
[UIView animateWithDuration: [self transitionDuration: transitionContext] animations:^{
// Lift up the presented controller.
presentedController.view.transform = CGAffineTransformMakeScale(1.0, 1.0);
// Brighten the presented controller (out of shadow).
presentedController.view.alpha = 1;
// Push the presenting controller down the screen – 3d effect to be added later.
presentingController.view.layer.transform = CATransform3DMakeTranslation(0,400,0);
} completion: ^(BOOL finished){
[transitionContext completeTransition: ![transitionContext transitionWasCancelled]];
}];
}
else
{
presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
presentedController.view.alpha = 0.7;
[UIView animateWithDuration: [self transitionDuration: transitionContext] animations:^{
// Bring the presenting controller back to its original position.
presentingController.view.layer.transform = CATransform3DIdentity;
// Lower the presented controller again and put it back in to shade.
presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
presentedController.view.alpha = 0.4;
} completion:^(BOOL finished) {
[transitionContext completeTransition: ![transitionContext transitionWasCancelled]];
}];
}
}
Initially, I thought about using CATransition to have custom transition effect when presentViewController:animated:completion: and dismissViewControllerAnimated:completion: a View Controller. But you want to show a portion of View Controller when the setting View Controller is presented, then I think CATransition would not help because you don't have full control of how far you want to move the View Controller.
I think the easiest way is to have one single View Controller with two full screen UIView. For the first UIView (View Controller's view, that is, self.view), you layout the setting, and on the second UIView, it's the regular View. In the ViewDidLoad, you add the 2nd view by using [self.view addSubview:2ndView];. Later when you want to present the setting view, you can do
CGRect frame = secondView.frame;
frame.origin.y = the_y_coordinate_you_like;
UIView animateWithDuration:0.2 animations:^{
secondView.frame = frame;
}];
then do the other way to bring the 2ndView back.

Flickering during custom view controller transition iOS 7

I'm trying to do a very simple transition: one view moves half the screen to the left while the second ("to") view moves in half a screen.
I have the animation working, but when I reverse the animation, I see a flickering. The "to" view (i.e. the original view) is visible at the origin of 0,0 although I set a different frame.
I dumped the view hierarchy. The frames are set correctly (-100 0; 320 480 for the to view), but nonetheless it shows up at 0,0. Is a screenshot of the view cached somewhere for the animation?
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];
CGRect offsetCoverRect = CGRectMake(-100.0, 0.0, 320, 480);
CGRect detailsRect = CGRectMake(-100.0 + 320.0, 0.0, 320, 480);
CGRect detailsOutsideRect = CGRectMake(320.0, 0.0, 320, 480);
CGRect normalRect = CGRectMake(0.0, 0.0, 320, 480);
if (self.revealDetails)
{
toViewController.view.frame = detailsOutsideRect;
[container addSubview:toViewController.view];
}
else
{
[container insertSubview:toViewController.view belowSubview:fromViewController.view];
// reversing… set the frame to the original offset (shows at 0,0 for a moment)
toViewController.view.frame = offsetCoverRect;
}
[UIView animateKeyframesWithDuration:2 delay:0 options:0 animations:^{
if (self.revealDetails)
{
fromViewController.view.frame = offsetCoverRect;
toViewController.view.frame = detailsRect;
}
else
{
fromViewController.view.frame = detailsOutsideRect;
toViewController.view.frame = normalRect;
}
} completion:^(BOOL finished) { [transitionContext completeTransition:finished]; }];
}
Update:
It seems to be related to UIModalPresentationCustom. I need to use this so that the from view is not removed when the transition completes. However, it seems to assume that the from view controller for the reverse transition starts at 0,0.
Update 2:
Very easy to reproduce with the following code:
UIView *snapshot = [containerView snapshotViewAfterScreenUpdates:NO];
[containerView addSubview:snapshot];
The above will show the to view centered on screen, no matter what actual frame or center I set before the animation.
I know this is an old question, but I ran into the same problem and after hours of struggling, finally came up with a solution:
In the completion block of the present animation,
Create a snapshot of the fromViewController view
Perform frame changes/transforms on the snapshot
Add snapshot to the containerView
Remove the fromViewController view from the container view
In the completion block of the dismiss animation (or anywhere if its a dismiss?), remove the snapshot from the container
By removing the fromViewController's view from the container view, it does not get reset to it's initial position because it is no longer inside of the container view.
This is because your animateTransition method is being called from background.
Call your method in this way
dispatch_async(dispatch_get_main_queue(), ^
{
[self animateTransition]; // your method goes here
});
You should never call any animation or any UI related methods on background thread

How do I perform these animated view transitions?

I am brand new to Core Animation, and I need to know how to do 2 animations:
I need to switch XIBs by fading through black (fully releasing the the first view controller)
I need to mimic the UINavigationController's pushViewController animation (switching XIBs and releasing the first view controller)
How can you achieve these animated view transitions?
I've done both of these animations, but maybe not in the exact way you are looking for.
Fade View to black, I took this the other way an instead added a new
subview that covered the entire window that was Black and animated
the Alpha from 0.0 to 1.0. Made for a nice effect.
[UIView animateWithDuration:0.5
animations:^{ _easterEgg.alpha = 1.0; }
completion:^(BOOL finished) { [self animateIndex:0]; }];
Slide in a view like UINavigationController. I didn't do this exactly like UINavigationController since it does multiple animations, but I did have a new view slide the previous view off screen. This code sets the frame of the new view off screen to the right of the current view, builds a frame location that is off the screen to the left, and grabs the current visible frame. Finally it just animates the new view from off screen right into the visible frame, and the old view from the visible frame to off left. Then removes the old view.
CGRect offRight = CGRectMake(_contentView.frame.size.width,
0,
_contentView.frame.size.width,
_contentView.frame.size.height);
CGRect offLeft = CGRectMake(-_contentView.frame.size.width,
0,
_contentView.frame.size.width,
_contentView.frame.size.height);
CGRect visibleFrame = CGRectMake(0, 0, _contentView.frame.size.width, _contentView.frame.size.height);
[view setFrame:offRight];
UIView *currentView = [[_contentView subviews] lastObject];
[_contentView addSubview:view];
[UIView animateWithDuration:0.5
animations:^{
[currentView setFrame:offLeft];
[view setFrame:visibleFrame];
}
completion:^(BOOL finished) {
[currentView removeFromSuperview];
}];

Resources