I am using an unwind segue to unwind to the initial view controller in my storyboard. The unwind works great, I implemented this method in my initial view controller:
- (IBAction) unwindToInitialViewController:(UIStoryboardSegue *) unwindSegue {
}
However if I try an segue to another view controller after I do the unwind I get the following error:
Warning: Attempt to present on
whose view is not in the window
hierarchy!
It seems like this only occurs if I unwind to the view controller that is checked as 'Initial View Controller' in the storyboard. Is this a bug? Should I be able to unwind to that initial controller? Other ideas?
EDIT:
Here is how I perform the second segue:
[self performSegueWithIdentifier:#"mySegue" sender:nil];
I should note that this is a login/logout problem. When I log in the first time the segue from my login controller to my next controller works. When I logout I unwind to the initial view controller. I then log in again and the segue from my login controller to the next controller does not work.
EDIT 2:
From more research I have found its because I am using a delegate for my login. Login is async, I make a call with AFNetworking and when its done I call my login delegate (the login VC in this case). At that point the login VC can segue to the view.
Login code:
- (void) login: (NSDictionary *) parameters {
[http.manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, NSDictionary *response) {
[self.loginDelegate loginSuccess:response];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self.loginDelegate loginFailure:error];
}];
}
My login VC which is the delegate:
- (void) loginSuccess:(NSDictionary *) response {
// setup user info based on response
...
// Segue
[self performSegueWithIdentifier:#"loginSuccessSegue" sender:nil];
}
I have checked that I am in the main thread when and I segue and still no luck. I know that AFNetworking always calls the success/failure blocks on the main thread too.
The tricky part. If I change that above code to use a block and not a delegate the storyboard/segue does not get messed up and I can login and logout many times with no problem.
Why does the segue work the first time with the delegate pattern, but on logout (unwind), can I not use that segue again?
EDIT 3:
More investigation shows that on unwind my login VC viewDidAppear is called twice. On initial unwind the view looks to still be on the stack, show it shows quickly and viewDidAppear is called. However this is animated away quickly and viewWillAppear is called a second time with a different VC. I think this might be the root of the problem. Why when I unwind to that VC is it animated away only to be animated back in?
Please check, whether your loginDelegate is nil during the second login attempt. If it is nil the "delegate calls" will just go to nowhere. Also please check whether the loginDelegate points to the instance you expect. If it points to an "old" instance, the wrong views may be tried to be presented.
The set of methods viewDidLoad, viewDidAppear, viewWillAppear, etc. can be called in unexpected order especially when going back in navigation or presenting an ad and coming back from it. If you have different initialization/setup tasks distributed among these methods, you may end up with a partly initialized view controller.
(Thinking about the problem I lost your statement about the error encountered, so probably the delegate is not nil.)
EDIT:
I ran one of my tiny unwind test projects and there log the viewDidAppear calls:
viewDidAppear: <ViewController: 0x7a687700>
viewDidAppear: <VC2: 0x7a70e970>
viewDidAppear: <VC3: 0x7a694d50>
unwind target
viewDidAppear: <VC2: 0x7a70e970>
viewDidAppear: <ViewController: 0x7a687700>
viewDidAppear: <VC2: 0x7a71b790>
viewDidAppear: <VC3: 0x7a694d20>
unwind target
viewDidAppear: <VC2: 0x7a71b790>
viewDidAppear: <ViewController: 0x7a687700>
Doing the unwind in VC3 briefly shows VC2 and eventually ends up at the target ViewController. Now the second "login" leads to different instances of the view controllers.
Do you keep references to "old" view controllers?
Another reason might be, that your "logout" detection fires twice (once when coming along the unwind and one more time when an intermediate or the initial view controller detects the need to login?).
Related
My view controller (A) issues a perform modal segue to view controller (B). The subsequent unwind segue action issues a second perform modal segue to what I expect to be another instance of the same view controller (B). That is, I want (B) to do it's thing a second time independent of the first segue to (B). While prepareForSegue is called a second time, the second segue is never executed. View (B) hangs around until the unwind segue action and the methods it calls complete, and the second perform segue and prepareForSegue are ignored.
The only way I can think to avoid this problem is to issue a perform block after a delay allowing the return segue et. al. to go to completion.
Is there a better way?
Thanks
If I understand, you are calling the segue to B from view controller B like so?
- (IBAction)unwindToThisViewController:(UIStoryboardSegue *)unwindSegue
{
// Code
[self performSegueWithIdentifier:#"segueToB"];
}
You can't go to view B because you are already there. You will need to wait until B has been fully dismissed and call it from view A.
I don't understand why this happens, but now it works. I have done nothing since yesterday other than add a few breakpoints. This is not the first time adding breakpoints seems to fix a problem.
So my setup is very simple I have:
Controller A
Controller B
Controller A segue's to Controller B, and it's a modal display.
When Controller B's viewDidLoad fires, I instantiate the NSURLConnection with request and start immediately.
If there's no network connection, then the didFailWithError gets fired.
From within here, i call dismissViewController, but when I do, I receive:
"Attempt to dismiss from view controller while a presentation or dismiss is in progress!"
What could possibly be causing this error?
I also have a button on Controller B, that when clicked also does the dismissViewController, but it does it correctly.
Could this be a race condition where Controller B has not finished animating/displaying once it hits viewDidLoad, and the URL connection spawning and failing immediately?
If this is the case, what's the correct way to fix this?
Thanks.
You can't dismiss a view controller until it has been completely presented. If you're still in viewDidLoad, this certainly isn't going to be the case, and I think this mostly will not be the case in viewWillAppear. viewDidAppear is the earliest possible state you can guarantee the view controller's presentation is complete and ready to be dismissed.
With that said... it's better from a UI perspective to instead try to start the NSURLConnection in the background and if you have a good network connection, then you present view controller B, and if not, simply never present it, rather than present and immediately dismiss.
Seems to be race condition. Can you try doing this in failToLoad delegate and see?
//Goes into the failed delegate
dispatch_async(dispatch_get_main_queue(), ^{
//dismiss the View Controller here??
});
I have a segue that should take place when one of a number of things happen, so it's called programatically, like so:
- (void)unwindAway
{
NSLog(#"Let's segue");
[self performSegueWithIdentifier:#"mySegue" sender:self];
NSLog(#"We should have just performed the segue");
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSLog(#"Let's do a segue");
}
but the output I get in the console is:
2014-05-29 22:20:30.173 My App[7848:60b] Let's segue
2014-05-29 22:20:30.178 My App[7848:60b] We should have just performed the segue
so as you can see, it's not even calling prepareForSegue.
The segue name is correct - if I give an invalid segue name it errors as you'd expect.
Any ideas?
For unwind segues, prepareForSegue:sender: is called on the view controller that was the source of the segue, in other words the one you're exiting from.
As per Rob's suggestion in the comments I checked the name of the method in the destination View Controller in the segue. It looked right (and was selected in IB rather than typed) but pasting over it and recompiling fixed the problem. Something must have been messed up in the source code of the storyboard, perhaps an artefact of renaming methods.
It's worth noting if anyone has a similar issue that the app won't generate any error if the destination method of a segue isn't found anywhere (I've confirmed this by typing a nonsense method name).
I know that your issue has been fixed, but for future reference I just want to say that similar problems might be caused by the way the unwind process works (zie the technical note link above).
As soon as the segue has been triggered in a certain view controller, its parent (!) view controller is called with the message: viewControllerForUnwindSegueAction:fromViewController:withSender:. The implementation checks if the parent wants to handle the unwind action. If not, it's array with child view controllers is searched for a view controller that wants to handle the action.
My problem was that the unwind action was implemented in a child view controller of a view controller that was embedded in a navigation controller. So, when te segue began, the navigation controller (the parent) was asked: will you handle the action? It returned NO. Then, it's children were asked the same. It returned NO. Because the message isn't sent to a child view controller of a child of the parent view controller, there isn't a view controller that will handle the unwind action and it is aborted without an error message.
My solution was to implement the unwind action in the view controller itself and not in it's child view controller.
I have master view controller (derived from UINavigationController) which seques to view controller A.
When the user exits A an unwind seque returns to the master controller which then seques to view controller B.
The problem I am facing is that if I have the following code in the master view controller:
- (IBAction)unwindToMasterViewController:(UIStoryboardSegue *)segue
{
[self performSegueWithIdentifier:#"SequeToViewControllerB" sender:self];
}
Then I get the error: "nested push animation can result in corrupted navigation bar Unbalanced calls to begin/end appearance transitions for View Controller B".
However if I remove the call to the performSeque from within the unwind seque and trigger it manually from a button on the master view controller then everything is ok. Therefore this suggests the problem is timing related, and in fact I've seen similar problems like this in the past which were related to animation timing (trying to call pushViewControllerAnimated:YES before a previous call to push has totally completed etc.).
So bearing that in mind I tried putting the seque in the code below, expecting didPopItem wouldn't get called until view controller A had completed being popped off the stack.
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item
{
[self performSegueWithIdentifier:#"HomeSeque" sender:self];
}
However that didn't solve it.
So how can I tell when view controller A has finished being popped off the stack so I know its safe to seque to view controller B? (Assuming that is indeed the problem, but seems like it is due to my button experiment).
Alternatively is there a way I can get the OS to transition from VC A to VC B for me?
You can use the approach discussed here: popping and pushing view controllers in same action
In other words, instead of using the canned unwind segue, which will call popViewControllerAnimated:YES, you pop by calling popViewControllerAnimated:NO and now you can go straight on to a push segue.
An even cleaner way is to call setViewControllers:animated: with the new stack of view controllers.
On my app's start up, it programmatically shows a LoginViewController using a segue. The view controller is presented modally with transition set to cross dissolve. Upon successful authentication, I want to dismiss the login view by programmatically triggering an unwind segue. So I added this to my header file:
- (IBAction)unwindSegue:(UIStoryboardSegue *)segue;
now in IB I'm able to control-drag from the "File's Owner" LoginViewController to the Exit button and choose unwindSegue:. This creates a manual segue, it shows up in the Connections inspectors for the File's Owner and the Exit button correctly. I then click on the newly created Unwind segue from the scene in IB and then give it a name. If I click on the "go to" button for the unwind segue action it takes me to the declaration mentioned above.
So far so good, I then trigger this unwind segue upon successful authentication in my GCD block:
....
dispatch_async(dispatch_get_main_queue(), ^
{
[self performSegueWithIdentifier:#"UnwindSegueIdentifier" sender:self];
[self.spinner removeFromSuperview];
self.spinner = nil;
});
.....and nothing happens when it runs. The spinner does get removed correctly, but there's no sign of that unwind segue executing.
A break point in the implementation of unwindSegue: never gets hit. There are no errors thrown. Nothing gets written to the console. The identifier is correct, I triple checked (otherwise it will fail anyway).
I looked at the answers here, here and here but I don't seem to have missed anything.
What I did notice though, is that Xcode thinks unwindSegue: is not linked:
I'm unable to drag from the little empty circle in front of unwindSegue: and link it to the Exit button.
Any help will be appreciated.
If you are using a modal segue to go to your login view, all you need to go back is to call
[self dismissViewControllerAnimated:YES completion:nil];
More precisely, you should call it at the presenting controller (your first controller), but it will be forwarded if you call at at the presented controller. You can use the completion block do any required clean up. There is no need to use GCD.
EDIT
To answer the additional comment: I'm not really sure from your description, but it seems you've implemented the unwind action at the presented controller instead at the presenting controller. Unwind segues are to allow to do something at the caller (e.g., setting data) without an additional protocol.
Quoting text from Apple's Technical Note on Unwind Segue:
To add an unwind segue that will only be triggered programmatically, control+drag from the scene's view controller icon to its exit icon, then select an unwind action for the new segue from the popup menu.
Link to the Technical Note