I have a controller embedded in navigation controller. Let's say that i have a button that repositions self.navigationController.navigationBar a bit. Then i do presentViewControllerAnimated with any controller (doesn't matter if it's nav or not) and after dismissing it navigation bar returns to it's original position (actually it is at its original position at dismiss animation start). In iOS 6 and earlier the bar would not be repositioned automatically. Any idea how can i prevent this repositioning in iOS 7?
OK, so I finally got it right.
First of all - Apple does not want us to change position of UINavigationBar. Therefore you should avoid it at all cost. In my case i got an app to fix which moved UINavigationBar to show slide-out menu. The proper solution to slide-out menu problem is to put UINavigationController inside - then you can slide whole UINavigationController with its content (whatever it is) and everything works fine. For some reason UINavigationController was outside in this app. So, i had to resort to a hack. Do not use this method if you have ANY option not to use it. It's a hack, it might break in further iOS versions and Apple would certainly not appreciate it.
First, explore new transitioning system in iOS7: http://www.doubleencore.com/2013/09/ios-7-custom-transitions/
Then, replace:
[self presentViewController:navigationController animated:YES completion:nil];
with
if([UIApplication iOS7]) /* or any other custom iOS7 detection method you implement */
{ /* we simulate old transition with nav bar slided out */
navigationController.transitioningDelegate = [OMModalTransitionDelegate new];
}
[self presentViewController:navigationController animated:YES completion:nil];
So, we need a transition delegate to simulate standard behaviour and do the trick as well.
#import "OMModalTransitionDelegate.h"
#import "OMAnimatedTransitioning.h"
#implementation OMModalTransitionDelegate
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
OMAnimatedTransitioning *transitioning = [OMAnimatedTransitioning new];
return transitioning;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
OMAnimatedTransitioning *transitioning = [OMAnimatedTransitioning new];
transitioning.reverse = YES;
return transitioning;
}
#end
And now the actual animation manager (you have to implement sharedBar in a category on UINavigationBar yourself):
static NSTimeInterval const DEAnimatedTransitionDuration = 0.4f;
static NSTimeInterval const DEAnimatedTransitionMarcoDuration = 0.15f;
#implementation OMAnimatedTransitioning
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];
UIView *superView = [UINavigationBar sharedBar].superview;
CGRect barFrame = [UINavigationBar sharedBar].frame;
if(self.reverse)
{ /* Trick - first, remove the bar from it's superview before animation starts */
[[UINavigationBar sharedBar] removeFromSuperview];
}
CGRect oldFrame = container.bounds;
if (self.reverse)
{
[container insertSubview:toViewController.view belowSubview:fromViewController.view];
}
else
{
toViewController.view.frame = oldFrame;
toViewController.view.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(oldFrame));
[container addSubview:toViewController.view];
}
[UIView animateKeyframesWithDuration:DEAnimatedTransitionDuration delay:0 options:0 animations:^
{
if (self.reverse)
{
fromViewController.view.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(oldFrame));
double delayInSeconds = 0.01; /* Trick - after an imperceivable delay - add it back - now it is immune to whatever Apple put there to move it */
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
[UINavigationBar sharedBar].frame = barFrame;
[superView addSubview:[UINavigationBar sharedBar]];
});
}
else
{
toViewController.view.transform = CGAffineTransformIdentity;
}
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
}
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return DEAnimatedTransitionDuration;
}
#end
In your custom navigation controller, add
- (void)viewWillLayoutSubviews {
//do your navigation bar layout
}
hope this can help you. Remind, above method only be supported ios >= 5.0.
Related
I've got a custom UIStoryboardSegue subclass which just replaces the root view controller with the destination VC. Works exactly as I want it to... however, I'd like to be able to add a transition animation, and I can't find any good examples of how to do that in the context of replacing the root VC.
The -perform selector of my class is this:
-(void)perform {
UIViewController *source = (UIViewController *)self.sourceViewController;
source.view.window.rootViewController = self.destinationViewController;
}
...How do I add a nice animated transition?
There are numerous ways you could add a "nice animation". Here is an example of a sort of card shuffle animation where one view moves up and left, while the other moves down and right, then reverses after changing the z-order of the two views. This implementation inserts the destination controller's view into the window's view hierarchy under the source controller's view.
-(void)perform {
CGFloat dur = 1.0;
UIView *destView = [self.destinationViewController view];
UIView *srcView = [self.sourceViewController view];
CGFloat viewWidth = srcView.frame.size.width;
CGPoint center = srcView.center;
AppDelegate *appDel = [[UIApplication sharedApplication] delegate];
destView.frame = srcView.bounds;
[appDel.window insertSubview:destView belowSubview:srcView];
[UIView animateWithDuration:dur animations:^{
srcView.frame = CGRectOffset(srcView.frame, -viewWidth/1.9, -20);
destView.frame = CGRectOffset(destView.frame, viewWidth/1.9, 20);
} completion:^(BOOL finished) {
[appDel.window bringSubviewToFront:destView];
[UIView animateWithDuration:dur animations:^{
destView.center = center;
srcView.center = center;
} completion:^(BOOL finished) {
[srcView removeFromSuperview];
appDel.window.rootViewController = self.destinationViewController;
}];
}];
}
I have implemented a custom transition for a UINavigationController that on presentation will scale the presenting view up and on dismiss will scale view controller being dismissed back down.
I have also implemented percent driven interaction.
I have a strange bug that only occurs in iOS 7.0.* (My app targets 7.0 and above). On certain screens only (but repeatably) if I cancel the percent driven transition by calling cancelInteractiveTransition on my UIPercentDrivenInteractiveTransition object, rather than cancelling the transition the top view controller view appears with the properties set in the UIView animation block used by my UIViewControllerContextTransitioning object for the dismissal animation.
So for example if I set the dismiss animation to scale the dismissing view controller down to 50%, I would see the top view controller scaled to 50% with a black screen behind it, instead of full size as it should be because I have cancelled the transition.
Everything works in iOS 7.1 and above, and on some of the screes in iOS 7.0.
Has anyone seen something like this before or have an idea what the problem might be?
Here is some code:
The implementation of the UINavigationControllerDelegate protocol methods:
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
{
if (UINavigationControllerOperationNone)
{
return nil;
}
if (operation == UINavigationControllerOperationPush)
{
self.transitionAnimator.type = AnimationTypePresent;
return self.transitionAnimator;
}
if (operation == UINavigationControllerOperationPop)
{
self.transitionAnimator.type = AnimationTypeDismiss;
return self.transitionAnimator;
}
return nil;
}
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController
*)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
return self.interactionController;
}
The code to begin the interactive transition:
- (void)edgePanGestureBegan:(UIScreenEdgePanGestureRecognizer *)recognizer
{
if (!self.transitionCoordinator) {
self.interactionController = [UIPercentDrivenInteractiveTransition new];
[self popViewControllerAnimated:YES];
}
}
The code to cancel the transition:
[self.interactionController cancelInteractiveTransition];
and the relevant code in my UIViewControllerContextTransitioning object:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIView *containerView = [transitionContext containerView];
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
if (self.type == AnimationTypePresent) {
...
} else if (self.type == AnimationTypeDismiss) {
[containerView insertSubview:toViewController.view belowSubview:fromViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
CATransform3D transform = CATransform3DMakeScale(0.01, 0.01, 1.0);
fromViewController.view.layer.transform = transform;
}
completion:^(BOOL finished) {
[transitionContext completeTransition:!transitionContext.transitionWasCancelled];
}];
}
}
I am trying to create a Zoom Transition. I had it working totally fine when it was for a "push". Now I need to to work for a modal transition, and of course it can't just be a 2 min fix.
The transition is from one NavigationController to another. I still don't understand when the ViewController keys point to NavigationControllers, they should point to the actual ViewControllers?
This does exactly what I want but viewWillAppear on the presented controller is NEVER called and the NavigationBar doesn't feel like appearing either.
Please help me. I would give every point I have to get this answered!
Here is the animation method:
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UINavigationController *fromNav = (id)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UINavigationController *toNav = (id)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController<SWZoomTransitionDelegate> *fromVC= (UIViewController<SWZoomTransitionDelegate> *)fromNav.topViewController;
UIViewController <SWZoomTransitionDelegate> *toVC = (UIViewController<SWZoomTransitionDelegate> *)toNav.topViewController;
UIView * containerView = [transitionContext containerView];
UIView * fromView = [fromVC view];
UIView * toView = [toVC view];
[containerView addSubview:toView];
UIView * zoomFromView = [fromVC viewForZoomTransition];
UIView * zoomToView = [toVC viewForZoomTransition];
UIImageView * animatingImageView = [self initialZoomSnapshotFromView:zoomFromView
destinationView:zoomToView];
if ([fromVC respondsToSelector:#selector(initialZoomViewSnapshotFromProposedSnapshot:)])
{
animatingImageView = [fromVC initialZoomViewSnapshotFromProposedSnapshot:animatingImageView];
}
animatingImageView.frame = [zoomFromView.superview convertRect:zoomFromView.frame
toView:containerView];
fromView.alpha = 1;
toView.alpha = 0;
zoomFromView.alpha = 0;
zoomToView.alpha = 0;
[containerView addSubview:animatingImageView];
ZoomAnimationBlock animationBlock = nil;
if ([fromVC respondsToSelector:#selector(animationBlockForZoomTransition)])
{
animationBlock = [fromVC animationBlockForZoomTransition];
}
[UIView animateKeyframesWithDuration:self.transitionDuration
delay:0
options:self.transitionAnimationOption
animations:^{
animatingImageView.frame = [zoomToView.superview convertRect:zoomToView.frame toView:containerView];
fromView.alpha = 0;
toView.alpha = 1;
if (animationBlock)
{
animationBlock(animatingImageView,zoomFromView,zoomToView);
}
} completion:^(BOOL finished) {
if ([transitionContext transitionWasCancelled]) {
[toView removeFromSuperview];
[transitionContext completeTransition:NO];
zoomFromView.alpha = 1;
} else {
[fromView removeFromSuperview];
[transitionContext completeTransition:YES];
zoomToView.alpha = 1;
}
[animatingImageView removeFromSuperview];
}];
}
I had a similar issue with another project. Calling viewWillAppear: is definitely expected behavior, according to the WWDC session:
And so as an interactive transition starts, the machinery behind in UIKit is actually going to be making calls out to view will appear, view will disappear, will show view controller, all the stuff that you've normally used to kind of control what's happening in your application as things come on and off screen.
In my case, the issue was that the animator object was getting deallocated during the transition. I was setting up the animator like this:
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source {
AGBAnimator *animator = [AGBAnimator new];
return animator;
}
This object goes out of scope and is deallocated during the animation. By creating a strong property and assigning animator to it before returning from this method, the issue was resolved and viewWillAppear:, etc. was called successfully.
I'm using the iOS 7 UIviewControllerAnimatedTransitioning protocol to present a modal ViewController with a custom animation. The animation works correctly, however, I want the newly presented ViewController to have a different status bar style than the presenting VC.
What I'm seeing is that -(UIStatusBarStyle)preferredStatusBarStyle gets called on the PRESENTING ViewController (several times in fact) and never on the newly presented ViewController. If I remove the custom animation everything with the status bar works as I'd expect.
Is there something special I need to do in my animateTransition function to update the root view controller or something? I've tried manually setting the statusBar with [UIApplication sharedApplication] setStatusBarStyle but it doesn't work (I think because I'm using the ios 7 view controller based status bar styling).
This is my code for animateTransition:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UICollectionViewCell *activeCell;
if ([self.collectionView.visibleCells containsObject:self.cellForActiveIdeaVC]) {
activeCell = self.cellForActiveIdeaVC;
}
UIView *container = transitionContext.containerView;
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
CGRect beginFrame;
if (activeCell) {
beginFrame = [container convertRect:activeCell.bounds fromView:activeCell];
} else {
beginFrame = CGRectMake(container.width / 2, container.height / 2, 0, 0);
}
CGRect endFrame = [transitionContext initialFrameForViewController:fromVC];
UIView *move = nil;
if (toVC.isBeingPresented) {
toView.frame = endFrame;
move = [toView snapshotViewAfterScreenUpdates:YES];
move.frame = beginFrame;
} else {
if (activeCell) {
move = [activeCell snapshotViewAfterScreenUpdates:YES];
} else {
move = [fromView snapshotViewAfterScreenUpdates:YES];
}
move.frame = fromView.frame;
[fromView removeFromSuperview];
}
[container addSubview:move];
[UIView animateWithDuration:.5
delay:0
usingSpringWithDamping:700
initialSpringVelocity:15
options:0
animations:^{
move.frame = toVC.isBeingPresented ? endFrame : beginFrame;
}
completion:^(BOOL finished) {
[move removeFromSuperview];
if (toVC.isBeingPresented) {
toView.frame = endFrame;
[container addSubview:toView];
} else {
if (self.cellForActiveIdeaVC) {
self.cellForActiveIdeaVC = nil;
}
}
[transitionContext completeTransition:YES];
}];
}
Any pointers much appreciated!
With iOS 7 custom transitions, it's possible to present a view controller that isn't fullscreen and therefore wouldn't affect the statusbar appearance. You have to explicitly tell iOS that your custom presented view controller will, in fact, control the status bar's appearance.
UIViewController *controllerToPresent = [...]
controllerToPresent.modalPresentationStyle = UIModalPresentationStyleCustom;
controllerToPresent.modalPresentationCapturesStatusBarAppearance = YES;
[self presentViewController:controllerToPresent animated:YES completion:nil];
There's some more information here. Hope that helps!
This worked for me:
[UIView animateWithDuration:0.25
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
_preferredBarStyle = UIStatusBarStyleLightContent;
[self setNeedsStatusBarAppearanceUpdate];
}];
And then you just have to return this value on the preferredStatusBarStyle method:
- (UIStatusBarStyle) preferredStatusBarStyle {
return _preferredBarStyle;
}
I hope it helps!
I have a requirement to show a status bar at certain times at the bottom of my application. I can easily put this at the bottom of my application's main view, but whenever I push a view controller on top of this (either modally or not) it hides this status bar.
Is there any way I can add a status bar like this, and have it be outside the bounds of my application itself? Ideally I'd like this to work like the call-in-progress status bar on the iPhone - when this bar appears, the app is pushed down, and a call to [[UIScreen mainScreen] applicationFrame] returns the correct size (i.e. it accounts for the presence of this status bar when calculating the height available for the app).
I wanted to do this, too, so I tried View Controller Containment. I'm still trying it out, so I'm not willing to give this a ringing endorsement, but it might be something you'd want to try playing around with yourself if you're in iOS5. But it appears to give you a status bar that will appear or disappear from the bottom of the screen.
This is a view controller that will open another view controller, but if there is status text to show, it pops up from the bottom of the screen and stays there until you get rid of it. I've only done a little testing so far, but it looks like this handles pushViewController/popViewController, but maybe not modal views.
My header looks like:
// StatusBarViewController.h
//
// Created by Robert Ryan on 7/8/12.
#import <UIKit/UIKit.h>
#interface StatusBarViewController : UIViewController
#property (strong, nonatomic) UIViewController *appController;
- (void)setStatus:(NSString *)text;
#end
My implementation file (this is ARC) looks like:
// StatusBarViewController.m
//
// Created by Robert Ryan on 7/8/12.
#import "StatusBarViewController.h"
#interface StatusBarViewController ()
{
BOOL _statusHidden;
UIView *_appView;
UILabel *_statusLabel;
}
#end
#implementation StatusBarViewController
#synthesize appController = _appController;
- (void)dealloc
{
_appView = nil;
_statusLabel = nil;
[self setAppController:nil]; // usually I don't like setters in dealloc, but this does some special stuff
}
- (void)createControlsWithStatusHidden
{
// create default app view that takes up whole screen
CGRect frame = self.view.frame;
frame.origin = CGPointMake(0.0, 0.0);
_appView = [[UIView alloc] initWithFrame:frame];
_appView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_appView.clipsToBounds = YES;
[self.view addSubview:_appView];
// create status label that is just off screen below the app view
_statusLabel = [[UILabel alloc] init];
_statusLabel.font = [UIFont fontWithName:#"Helvetica-Bold" size:12.0];
_statusLabel.backgroundColor = [UIColor darkGrayColor];
_statusLabel.textColor = [UIColor whiteColor];
CGSize size = [#"Hey!" sizeWithFont:_statusLabel.font]; // test size of box with random text
_statusLabel.frame = CGRectMake(0.0, frame.size.height, frame.size.width, size.height);
_statusLabel.textAlignment = UITextAlignmentCenter;
_statusLabel.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:_statusLabel];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self createControlsWithStatusHidden];
_statusHidden = YES;
// I'm instantiating from storyboard. If you're using NIBs, just create your controller controller using initWithNib and then set our appController accordingly.
self.appController = [self.storyboard instantiateViewControllerWithIdentifier:#"MainNavigator"];
}
- (void)setAppController:(UIViewController *)controller
{
if (controller)
{
controller.view.frame = CGRectMake(0.0, 0.0, _appView.frame.size.width, _appView.frame.size.height);
[self addChildViewController:controller];
[controller didMoveToParentViewController:self];
if (self.appController)
{
// if we have both a new controller and and old one, then let's transition, cleaning up the old one upon completion
[self transitionFromViewController:self.appController
toViewController:controller
duration:0.5
options:UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionCurveEaseInOut
animations:nil
completion:^(BOOL finished){
if (self.appController)
{
[self.appController willMoveToParentViewController:nil];
[self.appController removeFromParentViewController];
}
}];
}
else
{
// if we have no previous controller (i.e. this is our first rodeo), then just add it to the view
[_appView addSubview:controller.view];
}
}
else
{
// no new controller, so we're just removing any old on if it was there
if (self.appController)
{
// if there was an old controller, remove it's view, and remove it from the view controller hierarchy
[self.appController.view removeFromSuperview];
[self.appController willMoveToParentViewController:nil];
[self.appController removeFromParentViewController];
}
}
_appController = controller;
}
- (void)hideStatusWithCompletion:(void (^)(BOOL finished))completion
{
[UIView animateWithDuration:0.25
animations:^{
CGRect labelFrame = _statusLabel.frame;
labelFrame.origin.y += labelFrame.size.height;
_statusLabel.frame = labelFrame;
CGRect appFrame = _appView.frame;
appFrame.size.height += labelFrame.size.height;
_appView.frame = appFrame;
}
completion:completion];
}
- (void)unhideStatusWithCompletion:(void (^)(BOOL finished))completion
{
[UIView animateWithDuration:0.25
animations:^{
CGRect labelFrame = _statusLabel.frame;
labelFrame.origin.y -= labelFrame.size.height;
_statusLabel.frame = labelFrame;
CGRect appFrame = _appView.frame;
appFrame.size.height -= labelFrame.size.height;
_appView.frame = appFrame;
}
completion:completion];
}
- (void)setStatus:(NSString *)text
{
BOOL hasText = (text && [text length] > 0);
if (hasText)
{
if (!_statusHidden)
{
// if we have text, but status is already shown, then hide it and unhide it with new value
[self hideStatusWithCompletion:^(BOOL finished){
_statusLabel.text = text;
[self unhideStatusWithCompletion:nil];
}];
}
else
{
// if we have text, but no status is currently shown, then just unhide it
_statusLabel.text = text;
[self unhideStatusWithCompletion:nil];
}
_statusHidden = NO;
}
else
{
if (!_statusHidden)
{
// if we don't have text, but status bar is shown, then just hide it
[self hideStatusWithCompletion:^(BOOL finished){
_statusLabel.text = text;
}];
_statusHidden = YES;
}
}
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
#end
And then, any view controller that wants to update the status message would use a method kind of like:
- (void)setStatus:(NSString *)text
{
UIViewController *controller = [UIApplication sharedApplication].delegate.window.rootViewController;
if ([controller isKindOfClass:[StatusBarViewController class]])
{
[(StatusBarViewController *)controller setStatus:text];
}
}