In our app, there are many places that we need to quickly pop a view controller without animation and then push a new one with animation. We would do something like
[navController popViewControllerAnimated:NO];
[navController pushViewController:newVC animated:YES];
Pre-iOS8, this worked fine and the animation showed the new view controller sliding in over the current one, since the navigation controller was first popped without animation.
Now with iOS8, this seems to have changed and what happens now is the top view controller gets popped and the underlying view controller flashes for a split second and then the new view controller gets pushed on. I created a Xcode Project from scratch for iOS8 and tried to test this. Please see this GIF for a demonstration of what it looks like. Every time we tap one of the buttons in the master side of the split view, we perform the above two lines of code on the detail (right) side of the split. Note that the gray view (which is the root of the navigation controller) flashes for a brief second before the new one is pushed.
I have tried searching for any reason why this might have changed in iOS8 but I cant seem to find any documentation on it. Any one have any ideas on what might have caused this change? Any input would be greatly appreciated!
Also, I tried playing around with the code and discovered that doing the following code instead
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:navController.viewControllers];
[viewControllers removeLastObject];
[viewControllers addObject:newVC];
[navController setViewControllers:viewControllers animated:YES];
seems to fix the issue. However, I would prefer not to use this if possible since there are many places in our app that do this 2-line pop-push combo and I would prefer not to have to change it all over the place.
Thank you!
I came up with similar solutions to you, the first is situational however.
First, you could override the back buttons in the stack you're manipulating to pop to root (or whatever controller you want to be root).
Second, you could add a category to UINavigationController basically implementing the code you listed above. That would save you from having to change it everywhere in your app.
-(UIViewController *)popPushViewController:(UIViewController *)controller animated:(BOOL)animated {
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:self.viewControllers];
[viewControllers removeLastObject];
[viewControllers addObject:controller];
[self setViewControllers:viewControllers animated:animated];
}
I'm planning on implementing the latter if Apple doesn't fix it/respond.
Related
I've been struggling to find an answer to this question. I build a stack of modals via:
[[[NavA viewControllers] objectAtIndex:0] presentViewController:NavB animated:YES completion:NULL];
[[[NavB viewControllers] objectAtIndex:0] presentViewController:NavC animated:YES completion:NULL];
When I want to dismiss the NavA and NavB modals simultaneously I call
[[[NavA viewControllers] objectAtIndex:0] dismissViewControllerAnimated:YES completion:NULL];
This works fine except there's a brief flash where you can see NavB as the full stack is dismissed.
I stepped through the debugger and it looks like before the animation begins NavC disappears instantly and NavB dismisses with animation.
Is there any way to avoid this visual artifact and have the whole stack dismiss smoothly with NavC visible for the full duration of the animation?
Edit: To clarify, I'm presenting UINavigationController rather than UIViewController because this flow is for user login and has multiple possible branches that can lead back either to the current stage e.g. NavC (LoginPage), NavB (LandingPage with login and signup buttons) or all the way back to the root, NavA (main page of the application). In the iOS documentation they present a similar design pattern with the camera where each stage presents a UINavigationController with multiple possible view controllers https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ModalViewControllers/ModalViewControllers.html
Actually there's no way to do it by just using dismissViewControllerAnimated:completion: method no matter where you put it or how you call it (at least I couldn't, if someone knows a way - we all want to know).
HOWEVER, there's a hack you can use to achieve your desired outcome (this code should be called from "B" ViewController):
// Snapshot of "C" ViewController
UIGraphicsBeginImageContextWithOptions([UIScreen mainScreen].bounds.size, YES, 0);
UIView *snapshot = [self.presentedViewController.view snapshotViewAfterScreenUpdates:NO];
UIGraphicsEndImageContext();
// Cover the entire view of "B" (and hide navigation bar)
[self.view addSubview:snapshot];
self.navigationController.navigationBarHidden = YES;
// Dismiss "C" without animation
[self.presentedViewController dismissViewControllerAnimated:NO completion:^{
// Dismiss "B" with animation
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}];
If you are using a Storyboard then this should be achievable using Unwind Segues. Mike Woelmer has a good set of articles about this. Basically you provide information to the Storyboard about how a view can unwind through several different views to get a to a view that's already on the stack.
However, I'm a bit confused in the same way that Jeffery Thomas is in the comments: why are you presenting a navigation controller with another navigation controller? I can understand that you might want the navigation bar to look different on different views, but you can customise that when the view is due to appear. You should think a bit about the content of the views in NavB and NavC and ask yourself whether they are supposed to be presented as modal views or whether they would be better off as part of a single navigation stack. By presenting each Navigation Controller modally you're ending up with multiple navigation stacks, not a single stack with multiple view controllers. Even if just NavB and NavC were part of the same stack it would probably remove the visual glitch you're seeing.
If you did use a single navigation controller then you can get back to a previous view controller in the navigation stack by using the method -popToViewController:animated: on UINavigationController.
If you decide that presenting NavB and NavC modally as you are currently doing is the right thing to do then you are likely to get into trouble because when you ask NavA to dismiss its view controller it will try to dismiss NavB, which to it means setting up a transition between the NavB's view and NavA's view. That's why you're seeing that transition, and not the one you want (which is between NavC's view and NavA's). One way which might work (and sounds a bit weird) is to try to present NavA from NavC, then override the transition to make it look like you're popping NavC off the stack. Once you're there you can clean things up by removing any strong references to NavB and NavC. This article from Ash Furrow will get you most of the way.
You can fake the animation to look exactly as you wish:
pop/dismiss B and C without animation
push/present C without animation
pop/dismiss C using whatever animation you wish
I'm currently working on an app that builds a stack of controllers depending on the user.
Basically, I have a UIViewController that has a UIButton leading to another UIView Controller; that has a button leading to another view controller and so on. The view controllers are pushed so that when the user always press the button, I get a stack of multiple view controllers. The views are popped whenever the user wants to go back to the previous view controller.
Everything is working well (push and pop). However, at random instances, the app would crash. I noticed that it happens when there are already a large amount of views pushed, and I suspect that it can be a memory issue.
My question is, other than pushing the view controllers, is there an alternative so that I can avoid stacked views? Could it also be that the crash is not because of the stacked views but because I'm just missing something out? There is no error presented in the logs so I can't find out what's happening and I'm also new to iOS development.
Thank you very much!
Edit 1: There is no error in the logs but when the app crashes, there is this message:
Thread 1: EXC_BAD_ACCESS(code = 1, address = 0xd000000c)
Edit 2: This is how I am pushing the controller:
CustomController *custom = [self.storyboard instantiateViewControllerWithIdentifier:#"Custom"];
[self.navigationController pushViewController:custom animated:YES];
And this is how I popped it when the back button is pressed:
[self.navigationController popViewControllerAnimated:YES];
Edit 3: After enabling zombie objects in the scheme, I started to get this messages after multiple push and pop:
nested push animation can result in corrupted navigation bar
Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted.
Unbalanced calls to begin/end appearance transitions for
Do those messages say that the problem is actually on pushing the controller with animations? Thanks everyone!
Edit 4: I'll try to revise the question to make it more descriptive
This is my setup:
Controller A displays icons that corresponds to different places. You can click on the icon to push Controller B and display details for Location A.
Controller B displays information about Location A, with a button to show Controller A that now displays icons close to location of Location A. Now, you can again click an icon, say for Location B, and display details and so on.
When the user presses the back button, it should display the previous view controller. This is why I used push and pop. Is there a better way to handle this process. Thanks again!
My suggestion is: check if Zombie Objects is enabled and use the instrument "Allocations" to see if your application have, in fact, memory issues. With the informations provided by these tools, you can figure out what is happening with your application and work on it.
Tell me if you need help.
Good luck!
When push or pop, you should turn off animation. I think this causes crash when animation does not finish.
Push: self.navigationController pushViewController:custom animated:NO];
Pop: [self.navigationController popViewControllerAnimated:NO];
My guess is that you push multiple view controllers with animations - this may be a root cause of error. If you push more than one view controller you should animate only LAST push, say:
VC1 *vc1 = [self.storyboard instantiateViewControllerWithIdentifier:#"VC1"];
[self.navigationController pushViewController:vc1 animated:NO];
VC2 *vc2 = [self.storyboard instantiateViewControllerWithIdentifier:#"VC2"];
[self.navigationController pushViewController:vc1 animated:NO];
VC3 *vc3 = [self.storyboard instantiateViewControllerWithIdentifier:#"VC3"];
[self.navigationController pushViewController:vc1 animated:YES];
However, i hardly imagine the situation where the multiple push would be necessary - i think it always leads to bad UX.
i am having issues try to manipulate the navigationstack from an ios app. Or at least with the behaviour as a result of that manipulation.
My Situation:
i have 3 ViewControllers.
Controller a shows multiple levels,
Controller b is the gameview
Controller c is some sort of Score
Obviously i will select a level in controller a, which triggers a segue to controller b, once the level is finished ill segue to controller c. every segue as a push.
Now once im in controller c i dont want to be able to go back to b using the back button. I want to move back to controller a. In order for this to work, i removed the controller from the stack, so back wont move to controller b. This works fine.
The issue im facing is that the backbutton does show on controller a, which seems off since there shouldn't be any back. If i click the backbutton, the app doesnt crash, the button just disappears leaving the title.
i tried adding :
NSArray* controllers = [self.navigationController viewControllers];
if ([controllers count]<=1) {
[self.navigationItem setHidesBackButton:YES animated:YES];
} else {
[self.navigationItem setHidesBackButton:NO animated:YES];
}
[super viewDidAppear:animated];
as suggested in some relative stackoverflow article, without success. Besides this not working it seems off that ios creates those buttons from Storyboard without me actually adding them, but doesnt remove them when they arent necessary anymore. This leaves me with some options.
either i think that ios is smarter than it actually is
i am missing something essential to update the navigationbar
i went at this all wrong
besdies im using this code snipped to segue from Controller b to c.
[self performSegueWithIdentifier:#"feedbackSegue" sender:self];
[self removeFromParentViewController];
Any hints concerning missing operations or general bad practice is greatly appreciated.
Update
After further investigation, its not just the back button, its the whole navigationbar that is off. it behaves as if the removed controller was still there. The BackButton is there and another uiActionButton on the right end.
Does the navigationbar store its states onto a different stack, than the viewcontroller one? if this was the case, i could remove that state from this stack as well, to keep it consistent.
You could try this in your view controller c. This will remove the previous view controller, in your case the b. You'd also have to keep b in your stack (remove the line [self removeFromParentViewController]'; )
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
if(self.navigationController.viewControllers.count>2){
NSArray *controllers = self.navigationController.viewControllers;
NSMutableArray *newViewControllers = [NSMutableArray arrayWithArray:controllers];
[newViewControllers removeObject:[controllers objectAtIndex:self.navigationController.viewControllers.count - 2]];
self.navigationController.viewControllers = newViewControllers;
}
}
I believe the "correct" way to do this is to remove the back button on controller c. Depending on how and when you are removing controller b, you may be corrupting the navigation controller stack. It's generally not a good practice to manipulate the view controller stack.
To remove the back button, you have the correct code:
self.navigationItem.hidesBackButton = YES;
However, note that you must call this before the view controller is presented -- i.e., in something like viewDidLoad.
When you want to pop back to A, use:
[self.navigationController popToRootViewControllerAnimated:YES];
I've done a bit of research and read other answers I found here but haven't found anything that actually works. I have an app that when something is posted I want to go to the post and if the back button is pressed when viewing the post it should go back two views basically skip over the compose view.
Below is what I've tried but it gives
Warning: Attempt to present on whose view is not in the window hierarchy!
-(IBAction)post{
[[self presentingViewController] dismissModalViewControllerAnimated:NO];
}
-(void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
ViewPostViewController *dvController = [[ViewPostViewController alloc] initWithNibName:#"ViewPostViewController" bundle:[NSBundle mainBundle]];
UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:dvController];
nc.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:nc animated:NO];
[nc release];
}
If you trying to create a hierarchy of views like this, you should probably be using these:
[self.navigationController pushViewController:ViewController animated:BOOL completion:nil];
[self.navigationController popViewControllerAnimated:BOOL completion:nil];
Instead of:
[self presentViewController:ViewController animated:BOOL completion:nil];
[self dimissViewControllerAnimated:BOOL completion:nil];
PresentViewController is usually used to show a single view controller then dismiss it, not generally when you want to show several view controllers in a chain, then work your way back up the chain.
The former is advisable because it uses the stack concept to push and pop view controllers. So you can start with your initial list set up as the root view controller, push on your post compose view, then push on the third view to go to posting. Then when you want to go back to the first view controller by popping off two view controllers, you can use:
[self popToRootViewControllerAnimated:BOOL completion:nil];
You might find the UINavigationController reference useful.
Good luck.
If you want to present a view controller right after another modal view controller has animated out then you have to delay it because otherwise the new one will not appear.
before iOS 5 you would do a performSelectorAfterDelay: with something like 0.25 sec. For iOS 5 and above you wouldn't use modelViewController methods any more as those have been deprecated. Instead you use the presentViewController methods which give you an completion block that is called when the animation is done.
I'm a little confused about what you're trying to do. If you're using a navigation controller, you should be doing pushes and pops, not presenting and dismissing. If you want to use navigation controllers, then you can use popToViewController:animated: to go back to any particular controller without passing through the ones in between. You would have to create a custom back button, though, or do it in code, because the standard back button will only take you back to the previous view controller.
I have a navigation-based app that allows the user to drill down through hierarchies. Some of the child hierarchies have only one element in them, e.g.,
TopLevel1----->Level2a
TopLevel2 |->Level2b----->Level3a----->Level4a
|->Level2c
Instead of making the user tap 'Level3a', I just want to jump from Level2b to Level4a, but keep the Level3a view in the stack so when the user backtracks, it is visible.
I found some code here to simulate a row tap:
Simulate a Detail Disclosure Button press
When each level is loaded, I check to see if there is only one element in it. If so, I simulate the row tap. This all works initially, and the final view is loaded. But when I start backtracking through the view hierarchy, I get problems (it appears that the skipped views aren't loaded).
I think what I'm trying to accomplish is fairly simple, so I'm hoping someone on here can point me in the right direction.
You should be able to place a [self.navigationController pushViewController:level4 animated:NO] call in the viewWillAppear method for your level3 view controller. This will automatically push level4 on top of level3.
If it only happens some of the time, level3 can have a property to indicate when this behavior takes place.
I'm not 100% sure that would work, but that's what I would do.
You could directly [self.navigationController pushViewController:level4a animated:NO] and when that's done, set a new array of viewControllers, the navigationController propriety (an array that includes Level3a).
Here is a code sample, in you didSelectRowAtIndexPath:
[self.navigationController pushViewController:level4a animated:NO]; //Push the level 4 first
NSMutableArray* mutableViewControllers = [self.navigationController.viewControllers mutableCopy];
[mutableViewController addObject:level3a atIndex:3]; //Add the level 3 manually
self.navigationController.viewControllers = mutableViewControllers;
[mutableViewControllers release];