Unwind segue to child view controllers not behaving as expected - ios

Unwind segues seem not to behave as expected in iOS 8.1 when combined with a modal view and container view.
Here's the view controller hierarchy for the test project which can be found on github:
Tapping on the "tap me" button pushes the modal view which is embedded in a navigation controller and which has a tableView as a child view controller. Tapping on a row in the tableView pushes another tableView. Finally, tapping on a row in this final tableView should call the unwind segue named bUnwindSegue found on the previous view controller.
Problems:
bUnwindSegue is never called.
According to technical note TN2298 a container view controller is responsible for selecting the child view controller to handle a segue. In this case viewControllerForUnwindSegueAction:fromViewController:withSender: should be called on the container view controller. It isn't.
In the example project, you can see that BTableViewController contains the unwind segue:
- (IBAction)bUnwindSegue:(UIStoryboardSegue *)segue;
{
NSLog(#"Unwinding...this unwind segue will never get called.");
}
In the storyboard, the cell selection action for CTableViewController is indeed the bUnwindSegue. Also note that if you change the cell select action of CTableViewController to the unwind segue in the container view controller -- containerVCUnwindSegue -- that the segue is called correctly.
Are unwind segues not behaving as expected?

(1) You're misunderstanding the technical note TN2298 you cited and (2) you're not overriding viewControllerForUnwindSegueAction: appropriately.
As the TN2298 doc section you linked to about Container View Controller states underneath its "Selecting a Child View Controller to Handle An Unwind Action" subheading:
Your container view controller should override the method shown in
[viewControllerForUnwindSegueAction:] to search its child
view controllers for a view controller that wants to handle the unwind
action. If none of a container's child view controllers want to handle
the unwind action, it should invoke the super's implementation and return
the result.
First off, to override the method, you have to subclass the UINavigationController in your storyboard and add the viewControllerForUnwindSegueAction: method there. After doing that, you'll see the method is now being called as expected.
But your second error is that your current attempt to override the viewControllerForUnwindSegueAction: method simply contains return self;. You should instead be returning the view controller that you'd like to handle the unwind action.
So say, for example, you have a public variable in VCWithContainedVCsViewController to hold the current instance of BTableViewController and you access you're able to access that current container view controller, ex:
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
NSLog(#"Technical note TN2298 indicates child VCs defer to their parent to determine where an unwind segue will be handled.");
if ([NSStringFromSelector(action) isEqualToString:#"bUnwindSegue:"]) {
NSLog(#"%#", self.viewControllers);
VCWithContainedVCsViewController *containerVC = (VCWithContainedVCsViewController*)self.viewControllers[0];
return containerVC.container;
}
return [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
}
What you'll see in that case is that bUnwindSegue: is in fact being called (your message should print), but the segue still won't happen.
Why is this?
Because as I mentioned in the comments, BTableViewController is not on the current navigation stack. Some child view controllers of BTableViewController, like CTableViewController, will be on the navigation stack because, for example, CTableViewController is not a container view. But BTableViewController itself is not capable of performing the segue on its own because it is not on the current navigation stack. So although you can in fact select some child view controllers to handle unwind actions as the documentation states, BTableViewController isn't going to be one of them.

Related

Swift - Child controller causes 'Receiver has no segue with identifier'

I have a ViewController that has a child controller with a dynamic tableview.
On tapping a table cell I need to segue to a new view.
I've tried all kinds of methods, including directly control dragging from the cell (resulting in a 'detached controller' warning) with little success and the current situation is as follows.
Child Controller (currentName, currentType are globals that can be picked up by the parent, myHome)
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
currentName = "John"
currentType = 1
let segueVC = myHome()
segueVC.segueToMain()
}
Parent Controller (myHome)
func segueToMain() {
dispatch_async(dispatch_get_main_queue()){
self.performSegueWithIdentifier("home_myMain", sender: self)
}
}
I know this is probably horrendous but it appears to work fine in that it stores the data and gets to the parent function. However the attempt to segue causes the 'Receiver has no segue with identifier' error. The segue id is exactly correct. It links to a Navigation Controller.
I have a feeling that I need some delegates etc but I'm not really sure how to deal with them. Getting rid of the navigation controller and segue direct to view didn't make any difference.
Any help with how to get this segue to trigger would be greatly appreciated.
EDIT
In relation to answers suggesting the segue direct from the child, here is the setup which seems to result in a 'detached' controller error. For the record, I would love to use this method but all attempts failed.
The Parent Controller (A) builds a PageMenu class that gets its page structure from Controller B. B has a table with cells directly segued to another view.
A constructs the menu as follows
self.addChildViewController(self.pageMenu!)
self.view.addSubview(pageMenu!.view)
self.pageMenu!.didMoveToParentViewController(self)
PageMenu uses class B as the template for each menu page.
There is no physical link between A and B on the storyboard - only that the class for A sets B as a child.
Segues from B fail with a 'detached controller' error so maybe there's something I'm missing from the child setup?
Thanks
You can't just allocate a new instance of your parent view controller and ask it to perform a segue - As you are instantiating the view controller directly, it won't be associated with the storyboard, this is why you get an exception stating that that view controller doesn't have a segue of the specified identifier.
Even if you did instantiate a new instance from the storyboard you would still get an error as that new instance wouldn't be on screen, resulting in a message that you attempted to present a view controller on a view controller that isn't in the view hierarchy.
You need your child view controller to have a reference to the parent view controller instance that contains it and then invoke the segue using that instance.
Also, there is no need for the dispatch_async - the tableview delegate method will be invoked on the main queue already.
Finally, by convention, class names start with an upper case letter, so myHome should be MyHome (or even better, something like MyHomeViewController)

Why isn't my segue being performed?

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.

XCode 5 calling class with prepareforsegue

I have a tableview that is within a container view. When the user selects any of the rows, a method is called in the parent controller, which tells the parent controller to perform a segue. However, I am unable to figure out why it doesn't work. The code gets called from the didSelectRow -function in the tableView. The method does perform, but it gives me an error about no segue with that identifier.
However, when I call the method(listJobsOfSite) from within the parent view controller it works.
(tableview)
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.jobVC listJobsOfSite:#"locPwn"];
}
parent view controller
-(void)listJobsOfSite:(NSString *)site
{
[self performSegueWithIdentifier:#"JobSegue2" sender:nil];
}
EDIT:
The segue is between view controller 1, and view controller 2. view controller 1 holds a container view, which again holds a table view controller. This tableview controller should tell view controller 1 to segue into view controller 2.
EDIT 2:
screenshot http://tinypic.com/r/30x7dr9/8
You don't call classes. The word you are looking for is "method." You call your listJobsOfSite method that is in your parent view controller class .
Have you made sure you have given the segue an identifier? In your Storyboard you should click on the segue, inspect it, and enter "JobSegue2" in the field marked "Identifier."
If you have done step 2, do you need a container view? What are you trying to accomplish with the Container View + TableView? It sounds like your design would make more sense without the container view and the TableView as a property of View Controller 1.
If your heart is set on using the container view, in your Storyboard try to Control + Drag from your TableView cell to your View Controller 2. Name that segue "JobSegue2." Then you don't need to call any methods in your didSelectRow method. You also don't need a storyboard segue from View Controller 1 to 2. It seems like this question has the behavior you want (the question, not the answer! He is having the opposite problem you are).
Edit: Just noticed you have a Navigation Controller within your container view, so my suggestion above will likely push View Controller 2 within the container view. I'm totally confused by what you're trying to do.

Perform Unwind Segue Programatically in Unit Test

I am trying to unit test an unwind segue.
But it seems that in the test calling performSegueWithIdentifier: method doesn't perform an unwind segue while it does for other kinds of segues, e.g. Push and Modal segues.
Basically what I am trying to do is the following:
Get the view controller from storyboard.
Perform the unwind segue programmatically with this line of code:
[vc performSegueWithIdentifier:#"unwind" sender:vc];
(I have tried to change the sender to nil and vc.button which is the triggering button for the unwind segue. But the test still failed.)
Check if the seguePerformed property in the view controller is set to YES with this line of code:
XCTAssertTrue(sut.seguePerfomed, #"should be true.");
In this view controller's - prepareForSegue: method, I am using the identifier of a segue to identify the unwind segue. If it is the unwind segue then set the view controller's seguePerformed property to YES.
I thought there was something wrong with my performSegue code.
However, if I copy this line [vc performSegueWithIdentifier:#"unwind" sender:vc]; into my view controller's viewDidAppear method to test it, and enter this view in an iPhone simulator. It does return to its previous view just after entering it.
Sorry that I forgot to mention that I also tried the following snippet:
- (void)testPerformingSegueShouldSetSeguePerform
{
// given
UINavigationController *nvVC = [sut.storyboard instantiateViewControllerWithIdentifier:#"nvVC"];
[nvVC pushViewController:sut animated:NO];
sut = (NTDSubViewController *)nvVC.topViewController;
// when
[sut performSegueWithIdentifier:#"unwind" sender:sut.button];
// then
XCTAssertTrue(sut.seguePerfomed, #"should be true.");
}
An unwind segue only makes sense in live context, because it has to do with instances, not classes. For example, if I start in View Controller A and I push View Controller B, then I can unwind, popping to View Controller A - not just any View Controller A, but the very instance that we started with.
But you are just pulling View Controller B out of the storyboard and trying to unwind. You can't because there is no View Controller A to unwind to. For this to work, you must do exactly what I just described: start with View Controller A, push to View Controller B, and now tell that View Controller B to unwind.

How/when to push a view controller immediately after popping another?

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.

Resources