iOS: custom segue and presentViewController not working as expected - ios

I'm using a custom segue which looks like this:
#implementation ModalPushSegue
- (void)perform {
UIViewController *fromController = self.sourceViewController;
UIViewController *toController = self.destinationViewController;
UIView *fromView = fromController.view;
UIView *toView = toController.view;
CGPoint centerStage = toView.centerStage;
toView.center = toView.rightStage;
[fromView.window addSubview:toView];
[fromController addChildViewController:toController];
[UIView transitionWithView:toView
duration:0.5 options:0
animations:^{
toView.center = centerStage;
}
completion:nil];
}
This works well in that the view is slide on from the right as expected and the controller is added to the controller hierarchy.
But later in the added controller I do this:
[self presentViewController:anotherController animated:YES completion:nil];
I would expect this to slide the new controller's view up the screen ala modal style. But what happens instead is the the new view doesn't appear. And when I later remove this controller, it's view flashes up and slides off the screen, leaving a black background instead of the view that was originally there.
I've been playing around with this for a while and if I change the code to
//[self presentViewController:oauthController animated:YES completion:nil];
[self.view addSubview:oauthController.view];
[self addChildViewController:oauthController];
Then the view appears as expected, although not resized.
My problem appears to be with the way that the segues setup the hierarchy vs the way that presentViewController does things. I've done lots of reading and searching but so far have not been able to get a clear picture of exactly what is going on.
I've also played around with using presentViewController in the segue but instead of laying the new view over the old one, the screen goes black and the new view then slides on.
Any help appreciated.

Set a Storyboard ID on your destination view controller (in the storyboard), then try the following code:
AnotherController *viewC = [self.storyboard instantiateViewControllerWithIdentifier:#"AnotherController"];
ModalPushSegue *segue = [[ZHCustomSegue alloc] initWithIdentifier:#"coolSegueName" source:self destination:viewC];
[segue perform];

Related

Creating a new navigation stack with a flip transition instead of pushing another controller

I had this code to make a flip transition between UITabControllers:
UIStoryboard *sb = [UIStoryboard storyboardWithName:#"OtherSb" bundle:nil];
PrimaryTabBarController *tabBarController = [sb instantiateInitialViewController];
[UIView transitionWithView:[APP_DELEGATE window]
duration:0.8
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
[[APP_DELEGATE window] setRootViewController:tabBarController];
[[APP_DELEGATE window] makeKeyAndVisible];
}
completion:nil];
However, strangely, during the flip transition, the tab bar briefly flashes from the bottom to the top of the screen. I was able to make that stop by doing the following:
PrimaryTabBarController *tabBarController = [sb instantiateInitialViewController];
tabBarController.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:tabBarController animated:YES completion:nil];
The problem with this however is that I'm pushing another view controller onto the stack which can easily run through the memory. How can I create a new navigation stack without having the tab bar mess up the animation?
rootViewController is not very animatable property as you can imagine. makeKeyAndVisible is not animatable either and you should probably do that before you run any animations.
This API itself is very old (iOS 4.0) and I personally consider it more of a legacy of UIKit and I haven't seen it being used since iOS 6 when flipping views was still a thing.
Custom transitions introduced in iOS 7 is a very comfortable way of doing any kind of crazy animations however as you noticed, it creates a modal hierarchy that you don't always need.
All of these API were designed to work with sibling views within container. This is something mentioned in documentation and its samples. And it seems like that UIWindow is not suitable as global animation container.
Sample code from documentation:
[UIView transitionWithView:containerView
duration:0.2
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{ [fromView removeFromSuperview]; [containerView addSubview:toView]; }
completion:NULL];
I suggest that you follow the same logic as custom transitions and first setup dummy root controller that will serve you as a container for animation.
Then you add your views or entire view controllers inside of it and run animation between sibling views using
+ transitionFromView:toView:duration:options:completion:`
or
- transitionFromViewController:toViewController:duration:options:animations:completion:
or
+ transitionWithView:duration:options:animations:completion:
In addition to that, there is a useful flag UIViewAnimationOptionShowHideTransitionViews that will automatically hide the flipped view to avoid it to flicker or re-appear after animation.
When animation is over, you can swap the entire root controller in one call, that should be unnoticed to user.
This API has some quirks too, for example if by any chance you use it when the app is not on screen or you run it on a window that is not currently visible, then it will simply swallow the call. I used to have a check like
if(fromViewController.view.window) {
/* run animations */
} else {
/* swap controllers without animations */
}
I made a sample project to demonstrate how to use temporary container view for transition
https://github.com/pronebird/FlipRootController
Sample category on UIWindow:
#implementation UIWindow (Transitions)
- (void)transitionToRootController:(UIViewController *)newRootController animationOptions:(UIViewAnimationOptions)options {
// get references to controllers
UIViewController *fromVC = self.rootViewController;
UIViewController *toVC = newRootController;
// setup transition view
UIView *transitionView = [[UIView alloc] initWithFrame:self.bounds];
// add subviews into transition view
[transitionView addSubview:toVC.view];
[transitionView addSubview:fromVC.view];
// add transition view into window
[self addSubview:transitionView];
// flush any outstanding animations
// UIButton may cancel transition if this method is called from touchUpInside, etc..
[CATransaction flush];
[UIView transitionFromView:fromVC.view
toView:toVC.view
duration:0.5
options:options
completion:^(BOOL finished) {
// set new root controller after animation
self.rootViewController = toVC;
// move VC's view out of transition view
[self addSubview:toVC.view];
// remove transition view
[transitionView removeFromSuperview];
}];
}
#end

Switching between two view controllers without hierarchy

I have a few ViewControllers that all have buttons which should segue to some others. There will never be a back button, but instead everything is connected through a bunch of loops so that there is never a dead end. So I'd like to fully transition from one View Controller to another, and have the old View Controller be completely deleted. There is no hierarchy and no parent/child relationship between the View Controllers. How should I handle this situation?
Instantiate the view controller you want to go to, then set it as the window's root view controller.
NextViewController *next = [self.storyboard instantiateViewControllerWithIdentifier:#"Next"]; // or other instantiation method depending on how you create your controller
self.view.window.rootViewController = next;
You could do this with custom segues if you want to show the flow from controller to controller in your storyboard (you wouldn't need any code at all then). The custom segue's perform method would look like this,
#implementation RootVCReplaceSegue
-(void)perform {
UIViewController *source = (UIViewController *)self.sourceViewController;
source.view.window.rootViewController = self.destinationViewController;
}
If you want a fade animation, you can add a snapshot of the source view controller as a subview of the destination view controller's view, then fade it out,
-(void)perform {
UIViewController *source = (UIViewController *)self.sourceViewController;
UIView *sourceView = [source.view snapshotViewAfterScreenUpdates:YES];
[[self.destinationViewController view] addSubview:sourceView];
source.view.window.rootViewController = self.destinationViewController;
[UIView animateWithDuration:.5 animations:^{
sourceView.alpha = 0;
} completion:^(BOOL finished) {
[sourceView removeFromSuperview];
}];
}

Navigation bar jumps out from under status bar?

I have a button in my main view controller that pushes a navigation controller with an embedded view controller using a segue.
When the new view controller is presented, the navigation bar on it briefly appears under the status bar. (The status bar is not hidden.) The contents (which are relative to the top layout guide) are in the correct location. As soon as the animation is complete, it fixes itself.
When the view is dismissed again, the same thing happens: the main view controller briefly overwrites the status bar. For the main view controller, this is a little more significant as it's based on a UITableViewController; the entire table jumps. Again, when the animation is complete the view controller fixes itself.
I've tried turning off translucency on the navigation bar, but it only makes the problem more obvious. All of this works as expected on iOS 6.
I've uploaded a minimalist test case here: https://github.com/tewha/FlipTest
Another simple trick is do this:
In the MasterViewController
When is preparing for Segue:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
[UIView transitionWithView:self.navigationController.view
duration:0.75
options:UIViewAnimationOptionTransitionFlipFromRight
animations:nil
completion:nil];
}
And when Unwind the AboutViewController
- (IBAction)aboutUnwind:(UIStoryboardSegue *)segue {
[UIView transitionWithView:((UIViewController *)segue.sourceViewController).view
duration:0.75
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:nil
completion:nil];
}
This is a bug in the layout system of iOS7. I found that reducing the height of the navigation controller's view (not the pushed view controller's!) by the status bar height and placing it in y = status bar height will help a lot, but there will still be a small flicker where the status bar "merges" with the navigation controller.
As a side not, see if the bug still exists in iOS7.1b1.
There is a Problem in the ios 7 with navigation bar ,Navigation bar appear over the views or showing gap between the nav bar and view , You can solved this problem with the help of following code
There has been a new property introduced in iOS 7 that lets you adjust the layout behavior as in previous versions of iOS. this code in your view controller, and you should be good The space your navigation bar takes up should be accounted for automatically
if ([self respondsToSelector:#selector(edgesForExtendedLayout)])
self.edgesForExtendedLayout = UIRectEdgeNone;
Answer obtained from iOS 7 navigation bar jumping / stretching upon viewDidAppear
This is a bug in UK kit. Avoid using the standard methods
'performSegueWithIdentifier' or 'presentViewController'
Here I transition from one controller to another, then transition back in the delegate callback, using UIView transition animations.
-(void)photoButtonPressed:(NSNotification*)notification
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Media"
bundle:nil];
UINavigationController *navCon = [storyboard instantiateInitialViewController];
PhotoCaptureViewController *controller = navCon.viewControllers.firstObject;
controller.delegate = self;
CustomTabBarViewController *tabBarController = (CustomTabBarViewController*)self.tabBarController;
[UIView transitionWithView:self.navigationController.view duration:0.75 options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
[tabBarController.parentViewController addChildViewController:navCon];
[tabBarController.parentViewController.view addSubview:navCon.view];
} completion:^(BOOL finished) {
[navCon didMoveToParentViewController:tabBarController.parentViewController];
}];
}
-(void)photoCaptureViewController:(PhotoCaptureViewController *)controller dismissButtonPressed:(UIButton *)dismissButton
{
CustomTabBarViewController *tabBarController = (CustomTabBarViewController*)self.tabBarController;
[UIView transitionFromView:controller.navigationController.view toView:tabBarController.view duration:1 options:UIViewAnimationOptionTransitionFlipFromLeft completion:^(BOOL finished) {
[controller.navigationController willMoveToParentViewController:nil];
[controller.navigationController removeFromParentViewController];
[controller.navigationController.view removeFromSuperview];
}];
}
This is a great read about container views Khanlou's blog post

How to retain a UIView controller (ARC)

These 4 files are relevant to this post:
The FirstViewController has a button (not on the nav bar, a separate button), when it is pressed, the page should curl up to present FilterViewController.
FirstViewController.h
- (IBAction)searchOptions:(id)sender;
FirstViewController.m:
- (IBAction)searchOptions:(id)sender {
FilterViewController *ctrl = [[FilterViewController alloc] initWithNibName:#"FilterViewController" bundle:nil];
[UIView transitionFromView:self.view toView:ctrl.view duration:1 options:UIViewAnimationOptionTransitionCurlUp completion:nil];
[self.navigationController pushViewController:ctrl animated:NO];
}
On FilterViewController it has some UI stuff, you press a button, it saves the UI stuff and then the page curls back down to show the FirstViewController.
FilterViewController.h:
- (IBAction)backToMap:(id)sender;
FilterViewController.m:
- (IBAction)backToMap:(id)sender {
FirstViewController *ctrl = [[FirstViewController alloc] initWithNibName:#"FirstViewController" bundle:nil];
[UIView transitionFromView:self.view toView:ctrl.view duration:1 options:UIViewAnimationOptionTransitionCurlDown completion:nil];
[self.navigationController popViewControllerAnimated:YES];
}
The issue here is with the retention of UIView. How can I retain the UIView?
When I click the button on FirstViewController the animation works and the page is presented. However on FilterViewController when I click the button it crashes to the debugger with the error:
EXC_BAD_ACCESS(code=2,address=0x8)
In the output console it says: (lldb)
After the page curl up I have a stepper, when I click the stepper I get the same error in the debugger.
UPDATE: I have tracked the memory location error: http://i.imgur.com/dL18H9Z.png
Thanks.
One thing I notice is that you're pushing a view controller, then pushing another view controller with the syntax "back". This may be the issue: A nav stack is a stack. If you start with view 0, push view 1, if you want to get back to view 0 you "pop" view 1 as opposed to pushing view 0 again.
So in:
- (IBAction)backToMap:(id)sender {
FirstViewController *ctrl = [[FirstViewController alloc] initWithNibName:#"FirstViewController" bundle:nil];
[UIView transitionFromView:self.view toView:ctrl.view duration:1 options:UIViewAnimationOptionTransitionCurlDown completion:nil];
[self.navigationController popViewControllerAnimated:YES];
}
The issue here is that you try to make animation between view controllers with UIView's transition method.
According to documentation:
fromView
The starting view for the transition. By default, this view is removed
from its superview as part of the transition.
toView
The ending view for the transition. By default, this view is added
to the superview of fromView as part of the transition.
So, when you call this method, your ViewController's view replaced by another view with animation, and after on stack placed next ViewController without animation, so it's seems like all right (but your first controller's view already replaced).
But when you try to return some error behavior occurs - you replace view of controller, that will be removed.
So, i want to say, that i must be done more carefully, there several different approaches to make custom transition between viewControllers.
For example, you can watch next solution (it's similar to yours) - http://www.vigorouscoding.com/2011/05/custom-uiviewcontroller-transitions/
or
https://gist.github.com/jeksys/1507490

Custom animation for UINavigationController push not rendering navbar correctly

In a custom segue, I have the following simple transition:
- (void) perform {
UIViewController *src = (UIViewController *) self.sourceViewController;
UIViewController *dst = (UIViewController *) self.destinationViewController;
[UIView transitionWithView:src.navigationController.view duration:1
options:UIViewAnimationOptionTransitionFlipFromBottom
animations:^{
[src.navigationController pushViewController:dst animated:NO];
}
completion:NULL];
}
The content view animates fine. However, when executing the animation, the nav bar at the top has a messed up layout (buttons all crammed in the upper left corner, no title), popping into place only when the animation is finished. Anyone know what I've done wrong and how to fix it? Thanks!
Figured out my problem. The original code is indeed incorrect given how the UINavigationController works and interacts with the UIViewControllers it manages. (Annoyingly stuff like what I did in the OP can be found as a solution in older SO posts.)
Here's code that works for me (with one minor quibble):
- (void) perform {
UIViewController *src = (UIViewController *) self.sourceViewController;
UIViewController *dst = (UIViewController *) self.destinationViewController;
[UIView transitionFromView:src.view
toView:dst.view
duration:1
options:UIViewAnimationOptionTransitionFlipFromBottom
completion:nil];
[UIView transitionFromView:src.navigationItem.titleView
toView:dst.navigationItem.titleView
duration:1
options:UIViewAnimationOptionTransitionFlipFromBottom
completion:nil];
[src.navigationController pushViewController:dst animated:NO];
}
Quibble: this will animate the navbar separately from the content view, so you have two pieces flipping instead of the whole screen. I had originally tried to do:
[UIView transitionFromView:src.navigationController.view
toView:dst.navigationController.view
But that fails because the 1) destination's navigationController property isn't even set yet until it's pushed onto a nav controller, and 2) even if it were I'd be referring to the same view! I forgot that
The view for a navigation controller is just a container for several
other views, including a navigation bar, an optional toolbar, and the
view containing your custom content...Although the content of the
navigation bar and toolbar views changes, the views themselves do
not...the navigation controller object builds the contents of the
navigation bar dynamically using the navigation items (instances of
the UINavigationItem class) associated with the view controllers on
the navigation stack. To change the contents of the navigation bar,
you must therefore configure the navigation items for your custom view
controllers. (docs)
Another "quibble" ?
I put
[src.navigationController pushViewController:dst animated:YES];
before
[UIView transitionFromView ...
so that the navigation controller was reachable within the destination's viewDidLoad method.

Resources