Perform Unwind Segue Programatically in Unit Test - ios

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.

Related

UIStoryboard Show (e.g. push) simple inverse action to go back

In storyboard we have great feature that allow us to make Show (e.g. push). So seems the logic is next:
If we don't have navigation controller then view controller will use present modal logic. My question is there any inverse action that I can use with Show?
I have a UIButton that close current view controller screen:
- (IBAction)onTappedCloseButton:(id)sender
{
[self.navigationController popViewControllerAnimated:YES];
}
But in case if I don't have navigation controller, how can I simple use inverse action to go back? So my solution is to check if self.navigationController is nil then use dismissing option:
[self dismissViewControllerAnimated:YES completion:nil];
But maybe there is another cool solution like Show (e.g push). But Close (e.g. pop)?
Yes, you can use an unwind segue to go back, and it will be the reverse of whatever the forward segue was.
You have two options on how to do this:
1) The Unwind segue
To make an unwind segue you have to add a method in the view controller you want to "unwind" to with the following format:
-(IBAction)someSelectorName:(UIStoryboardSegue *)sender;
You will then be able to drag from your UIButton up to the "exit" icon in your storyboard.
Wire it up to the selector you just defined and UIKit will figure out how to get back to that view controller without you having to write any code. This can be especially useful as it can figure out when it needs to call -dismissViewControllerAnimated: more than once and can call those methods successfully. It can even unwind from within a view controller embedded in a navigation controller when the view controller you're unwinding to has the navigation controller presented on top of it. (i.e. it will do a dismissViewController instead of a pop to unwind)
2) The Custom unwind method
Say you don't want to or cant trigger this action from a storyboard. There is still an option and its detailed over at this question here:
Whats the programmatic opposite of showViewController:sender:
The gist is you can write your own generic dismiss method by implementing categories on the UIKit container View controllers (or on your own container)

Unwind segue to child view controllers not behaving as expected

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.

iOS Storyboards: How to segue to the second view with a back button to the first view, but without displaying the first view

Here is my storyboard configuration:
Navigation Controller -> View Controller A -> Push-> View Controller B
^
|
Modal
^
|
View Controller C
What I want to achieve: When a button is pressed in View C, directly View B will be opened modally (No part of View A is to be displayed). Also, View B will have a navigation back button to View A.
To achieve this,
I set up the illustrated storyboard.
I created a segue between View C and the Navigation Controller of View A/B.
In the 'prepareForSegue' method of View Controller C, I get an instance of View Controller A as the first element in the navigation. In this instance, I set a variable like 'directlyProceedToViewB=YES'.
In the viewDidLoad method of View Controller A, I check the variable 'directlyProceedToViewB' and if it is YES, I call 'performSegueWithIdentifier' to segue to View B
The result is so that, first View A is opened modally and after displaying it a very short time, View B is opened with a push animation (View B can navigate back to View A, which is good). But I do not want View A to be displayed any time at all. How can I achieve this?
EDIT:
To better visualize, I'm adding a screenshot with more example cases to support:
Here are some cases I want to support:
We can start with ViewC, click on 'Modally Display B' which opens ViewB, then click 'Back to A' to navigate back to ViewA, then click on 'Dismiss Modal' on ViewA to go back to ViewC
We can start with ViewD, clcik on 'Modally Display A' which opens ViewA, then click on 'PushB' to open ViewB, then go back and forth between A and B and modally dismiss to ViewD.
First of all, some corrections: those are not views but view controllers. And "view A" is not pushed into the UINavigationController but it's the root.
After that, I suggest making the segue in "view C" an unwind segue and implement the IBAction in "view A" by pushing "view B" with [[self navigationController] pushViewController:bViewController animated:NO].
EDIT (adding some details):
I assume that in ViewControllerA's viewWillAppear you present ViewControllerC in a not animated manner.
Implement an unwinding action like (IBAction)unwindAndThenGoToB:(UIStoryboardSegue *)segue in ViewControllerA.
In the storyboard connect the button in ViewControllerC to the Exit icon and select the previously defined method.
Then implement the method with the push call I wrote earlier.
ps: for documentation there is plenty on Apple's website.
Implement this using delegates.Decalre protocol in which class you want and define those methods and call the methods in the view controller you want.There is no many ways of calling some view and showing back button to go different view.modal view is just a concept.and you can use delegate methods to call whatever class you want.
Here I got a way to do so:-
You need to set no animation for segue from viewC to viewA as shown in below image. Then set a segue identifier for segue from viewA to viewB namely, "viewB" and in your viewA .m file add following code,
- (void)viewDidLoad {
[super viewDidLoad];
// Place your conditional check here.
[self performSegueWithIdentifier:#"viewB" sender:self]; //Will directly lead to viewB and viewA won't be shown as no animation is there from viewC to viewA.
}
And your rest flow be like-wise.
I found the solution myself.
First, I discovered that, my original proposal of
In the viewDidLoad method of View Controller A, I check the variable
'directlyProceedToViewB' and if it is YES, I call
'performSegueWithIdentifier' to segue to View B
works as I desired on iOS 7 but does not work on iOS 8.
So the solution is, in the viewDidLoad method of View Controller A, if 'directlyProceedToViewB' is YES, rather than calling performSegueWithIdentifier, use the following code:
ViewControllerB *destVC = [self.storyboard instantiateViewControllerWithIdentifier:#"ViewControllerBStoryboardID"];
[self.navigationController pushViewController:destVC animated:NO];

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.

Firing a segue from a different view

I have a segue that navigates from FirstViewController to SecondViewController. This happens by the press of a button. The code for the button is
- (IBAction)segue:(id)sender {
[self performSegueWithIdentifier:#"myIdentifier" sender:self];
}
This happens correctly when i press the button.
What i am trying to do is fire this method from another view. I tried doing
FirstViewController fvc = [[FirstViewController alloc] init];
[fvc segue:nil];
When i try this, I get the error message, Reciever has no segue with identifier 'myIdentifier'
How do i fire this segue programatically from another view?
There are a few issues here, but they boil down to a couple of key points:
Segues are a storyboard thing, so view controllers only "know" about segues if they're instantiated from a storyboard.
A view controller must be onscreen to perform a segue.
Segues are transitions from one specific view controller to another specific view controller.
So, in your snippet:
FirstViewController fvc = [[FirstViewController alloc] init];
[fvc segue:nil];
The first problem is that alloc-init-ing gives you an instance of FirstViewController that doesn't know anything about the storyboard it came from, so it doesn't know anything about segues. (This alone could be fixed using [instantiateViewControllerWithIdentifier:][1], but that doesn't solve your whole problem.) A second problem is that this instance doesn't fit anywhere into the UI hierarchy -- it hasn't been presented as a modal view controller, it's not the top view controller in the current navigation controller, it's not the visible window's root view controller, etc. In order to transition the screen from one view to another, the first view needs to be onscreen.
What it sounds like you want to do is transition from what's currently onscreen to the target of this segue. But the notion of a storyboard segue isn't just a transition with a destination -- it's a source, a destination, and a transition or relationship between them. If you have a different source view, you need a different segue. So, if you already have a segue from FirstViewController to SecondViewController, and you want to make a similar transition from OtherViewController (assuming that's the one onscreen now) to SecondViewController, you need to make a second segue connecting OtherViewController to SecondViewController.

Resources