I have a UIViewController which I'll call root, which is presenting (via modal segue) another UIViewController (firstChild), which is presenting (again via modal segue) a UINavigationController (topChild). In top child I do the following:
[root dismissViewControllerAnimated:NO completion:^{
[root performSegueWithIdentifier:#"ToNewFirstChild" sender:self];
}];
In iOS 7, the effect of this is that topChild remains on the screen until the segue to newFirstChild has been completed, and newFirstChild is then displayed (presented by root). I like that.
In iOS 8, the effect is that topChild is immediately removed from the screen, firstChild briefly is displayed and then removed, leaving root to be displayed until the segue has been completed, and newFirstChild is then displayed (presented by root). I don't like that.
If I choose to animate the dismissViewControllerAnimated:completion:, the following results happen: In iOS 7, topChild is dismissed with animation, without ever revealing firstChild (as advertised in the documentation), leaving root to be displayed until the segue has been completed; and in iOS8, topChild is immediately removed from the screen, leaving firstChild, which is dismissed with animation (contrary to the documentation!), again leaving root to be displayed until the segue has been completed.
Any idea how I can get the effect produced in iOS 7 (either with or without animation) in iOS 8? What am I missing?
The trouble you're dealing with here is that you're trying to call a method in the completion block of dismissViewControllerAnimated:completion of a View Controller that you're dismissing (and thus deallocating), thus the sporadic and unpredictable behaviour.
I'd recommend using something like NSNotificationCenter to post a notification when the view controller is being dismissed, and retain a handler in your parent (root) controller that can receive the notification.
In your root view controller, call this somewhere (maybe in prepareForSegue?):
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(topChildWasDismissed:)
name:#"TopChildDismissed"
object:nil];
You'll also need a handler method in your root VC:
- (void)topChildWasDismissed {
[self performSegueWithIdentifier:#"ToNewFirstChild" sender:self];
}
Then in your top child:
[self dismissViewControllerAnimated:YES completion:^(void) {
[[NSNotificationCenter defaultCenter] postNotificationName:#"TopChildDismissed" object:self];
}];
Related
I have a custom in app notification appear at the top of any view controller on the screen when certain things happen.
Tapping on it triggers a notification:
[[NSNotificationCenter defaultCenter] postNotificationName:#"DidTapOnNotification" object:nil];
The observer for that specific notification is the root navigation controller for my application, which I subclassed. I addObserver in viewDidLoad. This notification is always received, and the code I run in response is:
[CATransaction begin];
[CATransaction setCompletionBlock:^{ // this is called when the popToRootViewController animation completes
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"NavigateToController" object:nil userInfo:notification.userInfo];
});
}];
[self popToRootViewControllerAnimated:YES];
[CATransaction commit];
I added a delay (dispatch_after) arbitrarily to see whether I just had to give my root view controller time to appear (i confirmed that it appeared before the 2 seconds are up).
Now, in my root view controller I again, add it as an observer for the NavigateToGroup notification. I.e. I call [[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(openController:) name:#"NavigateToController" object:nil]; in viewDidLoad.
The problem is that the selector (i.e. the openController: method) is not always called. The only time it's called is when I'm already on that controller (the root controller) when I tap on the in app notification, then it works as expected. If I have other controllers on the navigation stack, tapping will popToRoot as expected, but then the method openController: will never get called even though I'm sure the notification gets posted (and i'm sure the view is appeared when it does).
Does anyone know what's going on here?
Or conversely, can anyone recommend a better way of handling this?
Turns out HariKrishnan.P's comment had some truth to it. NSNotifications don't seem to be posted if the view controller that is sending the message has already been popped. (Not sure if this is always the case but definitely seemed to be the case here)
I ended up refactoring the code to only send one initial notification then bypassing this by using delegation and calling my method explicitly through it rather then relying on on an NSNotification, which is probably a better solution anyways.
I have an app that has an initial login screen then when the user wants to sign up, they are presented with a registration form that is three view controllers presented modally. When the user completes the form on the third screen (by pressing a "Done" button), I want the user to be taken back to the initial login screen.
I have tried doing this in the third view controller:
[self dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil]
However it only dismissed two of the view controllers and not all 3. Why did this happen?
As other people pointed out, there are more elegant/efficient/easier ways to achieve similar results from the UX perspective: via a navigation controller, or a page view controller, or other container.
Short/quick answer: you need to go one step further in the chain of presenting view controllers, because the dismissal request needs to be sent to the controller that's presenting, and not to the one that's being presented. And you can send the dismiss request to that controller only, it will take care of popping from the stack the child controllers.
UIViewController *ctrl = self.presentingViewController.presentingViewController.presentingViewController;
[ctrl dismissViewControllerAnimated:NO completion:nil]
To explain why, and hopefully help other people better understand the controller presenting logic in iOS, below you can find are more details.
Let's start from Apple documentation on dismissViewControllerAnimated:completion:
Dismisses the view controller that was presented modally by the view controller.
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
Thus [self dismissViewControllerAnimated:NO completion:nil] simply forwarded the request to self.presentingViewController. Which means the first two lines had the same effect (actually the 2nd line did nothing as there was no presented controller after the 1st one executed).
This is why your dismissal of view controllers worked only the top 2 ones. You should've start with self.presentingViewController and go along the chain of presenting view controllers. But this is not very elegant and can cause problems if later on the hierarchy of view controllers changes.
Continuing to read on the documentation, we stumble upon this:
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.
So you needn't call dismissViewControllerAnimated:completion: three times, a call on the controller that you want to come back will suffice. At this point, passing a reference to that controller would be more reliable than navigating through the stack of view controllers.
There are some more useful details in the documentation, for example regarding what transitions apply when dismissing multiple controllers at once.
I recommend you go through the whole documentation, not only for this method, but for all methods/classes that you use in your application. You'll likely discover things that will make your life easier.
And if you don't have the time to read all Apple's documentation on UIKit, you can read it when you run into problems, like in this case with dismissViewControllerAnimated:completion: not working as you thought it would.
As a closing note, there are some more subtle issues with your approach, as the actual dismissal takes place in another runloop cycle, as it's possible to generate console warnings and not behave as expected. This is why further actions regarding presenting/dismissing other controllers should be done in the completion block, to give a change to UIKit to finish updating its internal state.
Totally understood. What I will do is embed a navigation controller instead of using modal. I have a case just like you. I have LoginViewController to be the root view controller of the UINavigationController. SignupViewController will be presented by push method. For ResetPasswordViewController, I will use modal because it's supposed to go back to LoginViewController no matter the results. Then, you can dismiss the whole UINavigationController from SignupViewController or LoginViewController.
Second approach will be like, you come up with your own mechanism to reference the presented UIViewController via a shared instance. Then, you can easily dismiss it. Be careful with the memory management. After dismissing it, you should consider whether you need to nil it right away.
I know three ways to dismiss several viewControllers:
Use a chain of completion blocks
~
UIViewController *theVC = self.presentingViewController;
UIViewController *theOtherVC = theVC.presentingViewController;
[self dismissViewControllerAnimated:NO
completion:^
{
[theVC dismissViewControllerAnimated:NO
completion:^
{
[theOtherVC dismissViewControllerAnimated:NO completion:nil];
}];
}];
Use 'viewWillAppear:' method of viewControllers
~
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.shouldDismiss)
{
CustomVC *theVC = (id)self.presentingViewController;
theVC.shouldDismiss = YES;
[self dismissViewControllerAnimated:NO completion:nil];
}
}
Pass a reference to LoginVC1 further down the chain.
(This is the best approach so far)
Imagine you have some StandardVC, which presented LoginVC1.
Then, LoginVC1 presented LoginVC2.
Then, LoginVC2 presented LoginVC3.
An easy way of doing what you want would be to call (from inside your LoginVC3.m file)
[myLoginVC1 dismissViewControllerAnimated:YES completion:nil];
In this case your LoginVC1 would lose its strong reference (from StandardVC), which means that both LoginVC2 and LoginVC3 would also be deallocated.
So, all you need to do is let your LoginVC3 know that LoginVC1 exists.
If you don't want to pass a reference of LoginVC1, you can use:
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil];
However, the above approaches are NOT the correct ways of doing what you want to do.
I would recommend you doing the following:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (!self.isUserLoggedIn)
{
[UIApplication sharedApplication].keyWindow.rootViewController = self.myLoginVC;
}
return YES;
}
Then, when user finished his login process, you can use
[UIApplication sharedApplication].keyWindow.rootViewController = self.myUsualStartVC;
I'm using storyboard.
As I remeber (I worked with ios 4, long time ago=)) everytime, when View appears, calls
-(void)viewWillAppear:(BOOL)Animated {}
method.
Now this method doesn't call, if I press Home button and run app again.
How to fix it?
I need to update one UIView if it appears after home pressing.
The function viewWillAppear is not part of UIView. It is part of UIViewController.
It is called after the view controller's view has been loaded and just before it starts to transition onto screen.
If you create a subclass of UIView and put this function in it then it will never be called because it isn't supposed to be.
Edit
You are correct that viewWillAppear does not get called when the app is coming back from the background.
If you want to update a part of your app when this happens then you can do something in the AppDelegate.
I'd recommend not trying to store properties etc... in the AppDelegate though. You should do something like this...
- (void)applicationWillEnterForeground:(UIApplication *)application
{
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
// just pass the message on. In your view you will need to add an observer for this notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"UpdateViewNotification" object:nil];
}
try this, call the super,
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
}
I'm attempting to convert our application to storyboards and have hit what I believe is a bug in the handling of unwind segues when dealing with custom container controllers. We have a view controller which displays another and uses the view controller containment api to do this, I wire up the segue in IB then select a custom class for the implementation. The perform method looks something like this:
-(void) perform {
UIViewController *container = [self sourceViewController];
UIViewController *child = [self destinationViewController];
[container addChildViewController:child];
[container.view addSubview:child.view];
child.view.center = container.view.center;
[UIView transitionWithView:container.view
duration:0.35
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
child.view.alpha = 1;
} completion:^(BOOL finished) {
[child didMoveToParentViewController:container];
}];
}
That works perfectly, however I can't make it perform the unwind segue back to the container controller. I override viewControllerForUnwindSegueAction: fromViewController: withSender: and ensure that it's returning the correct value:
-(UIViewController *) viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
id default = [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
NSAssert1(default == self, #"Expected the default view controller to be self but was %#", default);
return default;
}
I can also confirm that canPerformUnwindSegueAction:fromViewController:withSender is being called and doing the right thing, but to be sure I overrode it to return YES
-(BOOL) canPerformUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
return YES;
}
The next step I would expect to happen is for segueForUnwindingToViewController:fromViewController:identifier: to be called, however it never is. Instead the application crashes with an NSInternalInconsistencyException.
2012-10-01 10:56:33.627 UnwindSegues[12770:c07] *** Assertion failure in -[UIStoryboardUnwindSegueTemplate _perform:], /SourceCache/UIKit_Sim/UIKit-2372/UIStoryboardUnwindSegueTemplate.m:78
2012-10-01 10:56:33.628 UnwindSegues[12770:c07] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Could not find a view controller to execute unwinding for <USCustomContainerViewController: 0x75949a0>'
*** First throw call stack:
(0x1c8e012 0x10cbe7e 0x1c8de78 0xb61f35 0x581711 0x45ab54 0x10df705 0x16920 0x168b8 0xd7671 0xd7bcf 0xd6d38 0x4633f 0x46552 0x243aa 0x15cf8 0x1be9df9 0x1be9ad0 0x1c03bf5 0x1c03962 0x1c34bb6 0x1c33f44 0x1c33e1b 0x1be87e3 0x1be8668 0x1365c 0x1e7d 0x1da5)
libc++abi.dylib: terminate called throwing an exception
Has anyone successfully used unwind segues combined with the view controller containment APIs? Any idea what step I'm missing? I've uploaded a demo project to github which shows the issue in the simplest demonstration project I could come up with.
The problem in your example is that there's no there there. It's too simple. First, you create your container view controller in a rather odd way (you don't use the new IB "container view" which is there to help you do this). Second, you've got nothing to unwind: nothing was pushed or presented on top of anything.
I have a working example showing that canPerformUnwindSegueAction really is consulted up the parent chain, and that viewControllerForUnwindSegueAction and segueForUnwindingToViewController are called and effective, if present in the right place. See:
https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch19p640presentedViewControllerStoryboard2
I have now also created a fork of your original example on github, correcting it so that it illustrates these features:
https://github.com/mattneub/UnwindSegues
It isn't really a situation where "unwind" is needed, but it does show how "unwind" can be used when a custom container view controller is involved.
This seems to be a bug – I would also expect unwind segues to work as you implemented.
The workaround that I used is explicitly dismissing the presented view controller in the IBAction method:
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController
fromViewController:(UIViewController *)fromViewController
identifier:(NSString *)identifier
{
return [[UIStoryboardSegue alloc] initWithIdentifier:identifier
source:fromViewController
destination:toViewController];
}
- (IBAction)unwind:(UIStoryboardSegue*)segue
{
UIViewController *vc = segue.sourceViewController;
[vc willMoveToParentViewController:nil];
if ([vc respondsToSelector:#selector(beginAppearanceTransition:animated:)]) {
[vc beginAppearanceTransition:NO animated:YES]; // iOS 6
}
UIView *modal = vc.view;
UIView *target = [[segue destinationViewController] view];
[UIView animateWithDuration:duration animations:^{
modal.frame = CGRectMake(0, target.bounds.size.height, modal.frame.size.width, modal.frame.size.height);
} completion:^(BOOL finished) {
[modal removeFromSuperview];
[vc removeFromParentViewController];
if ([vc respondsToSelector:#selector(endAppearanceTransition)]) {
[vc endAppearanceTransition];
}
}];
}
Brief history before the answer: I just ran into the same exact error message when trying to use multiple Container Views on one iPad screen in iOS 6 and calling unwind segues from code. At first I thought this may be a problem because my segue was created using Storyboards by CTRL-dragging from File Owner to Exit instead of from some UI control to Exit, but I got same results when I put test Close buttons on each VC and had them trigger the unwind segues. I realized that I'm trying to unwind an embed segue, not a modal/push/popup segue, so it makes sense that it fails to do it. After all, if the unwind segue succeeds and the view controller is unloaded from a Container View, iOS 6 thinks there'll just be an empty space on the screen in that spot. (In my case, I have another container view taking up screen real estate that's shown behind the container view which I'm trying to unload, but iOS doesn't know that since the two aren't connected in any way.)
Answer: this led me to realize that you can only unwind modal, push, or popover segues, be it within the main window or as part of a Navigation/Tab Controller. This is b/c iOS then knows that there was a previous VC responsible for the whole screen and it's safe to go back to it. So, in your case, I'd look into a way to tell iOS that your child container view is connected to your parent container view in a way that makes it safe to dismiss the child container view. For example, perform a modal/push/popover segue when displaying the child container view, or wrap both into a custom UINavigationController class (I assume you don't want the navigation bar, that's why custom class).
Sorry I can't give exact code, but this is the best I got to so far and I hope it's helpful.
Looks like this bug is fixed in iOS9.
I'm wondering if there is any way to return from UIViewController like 3-4 step backward, I've a main screen which will navigate to other UIViewController via presentModalViewController, on the next view, it will have a UINavigationBar which will navigate to a 4-5 level deeps. i wanna to put a button that let the user go back to the home directly without returning for all the view he enter.
thx in advance.
Have your root level view controller register as an observer of a notification, such as "POP_TO_ROOT". When it receives this notification, call a method to dismiss your modal view controller (or whatever is first on the stack).
In your viewcontroller stack, any of the views 4 or 5 levels in can just post a notification "POP_TO_ROOT".
EDIT: add code
In your main "screen" before you call presentModalViewController, do this:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(handlePopToRoot)
name:#"POP_TO_ROOT"
object:nil];
and add this method:
- (void) handlePopToRoot {
[[NotificationCenter defaultCenter] removeObserver:self
name:#"POP_TO_ROOT"
object:nil];
[self.navigationController dismissModalViewControllerAnimated: YES];
}
Then, down deep in your viewcontroller hierarchy, when you want to pop all the way out,
you just need to post a notification:
[[NSNotificationCenter defaultCenter] postNotification:#"POP_TO_ROOT" object:nil];
If I understand your question correctly, you are presenting a navigation controller (with a root view controller attached to it) from your "main view controller" modally, and you want to be able to get back to your "main view controller".
Because you will always have pointer to your navigation controller, you should be able to call
dismissModalViewControllerAnimated: from any of your view controllers and it will take you right back to the main view controller.
[[self.navigationController parentViewController] dismissModalViewControllerAnimated:YES]
Save your root View Controller in some property and call:
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated