How to unwind through multiple views without displaying intermediate views - ios

Assume we have three view controllers: 1, 2, and 3. Using the storyboard, it's pretty simple to unwind from view controller 3 to view controller 1 using an unwind segue. However, when unwinding, view controller 2 is briefly visible before view controller 1 is displayed. Is there any way to get from 3 to 1 without displaying 2 again?
View Controller 1:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"one did appear");
}
- (IBAction)goToTwo:(id)sender {
NSLog(#"#### segue to two");
[self performSegueWithIdentifier:#"TwoSegue" sender:self];
}
- (IBAction)unwindToOne:(UIStoryboardSegue *)sender {
}
View Controller 2:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"two did appear");
}
- (IBAction)goToThree:(id)sender {
NSLog(#"#### segue to three");
[self performSegueWithIdentifier:#"ThreeSegue" sender:self];
}
View Controller 3:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"three did appear");
}
- (IBAction)unwindToOne:(id)sender {
NSLog(#"#### unwind to one");
[self performSegueWithIdentifier:#"OneSegue" sender:self];
}
This produces the following log messages:
one did appear
segue to two
two did appear
segue to three
three did appear
unwind to one
two did appear
one did appear
I've tried using custom segues and disabling animation. Although removing animation makes view controller 2 appear for an even shorter period of time, it still appears. Is there any way to program this behavior?
Screenshot of storyboard:

This seems to be due to the way that unwind segues search for the nearest view controller which implements the unwind action you specified in the storyboard. From the documentation:
How an Unwind Segue Selects its Destination
When an unwind segue is triggered, it must locate the nearest view controller that implements the unwind action specified when the unwind segue was defined. This view controller becomes the destination of the unwind segue. If no suitable view controller is found, the unwind segue is aborted. The search order is as follows:
A viewControllerForUnwindSegueAction:fromViewController:withSender: message is sent to the parent of the source view controller.
A viewControllerForUnwindSegueAction:fromViewController:withSender: message is sent to the next parent view controller [...]
You can override canPerformUnwindSegueAction:fromViewController:withSender: if you have specific requirements for whether your view controller should handle an unwind action.
The view controllers in your example don't have a parent, so it seems that the fallback (which I can't see documentation for) is to instantiate each presentingViewController and call canPerformUnwindSegueAction on them in turn. Returning NO from this method in ViewControllerTwo doesn't prevent its instantiation and display, so that doesn't solve the issue.
I've tried your code embedded within a navigation controller, and it works fine. This is because in that case, UINavigationController is the parent of each of your view controllers, and it handles all the selection of a destination view controller. More documentation:
Container View Controllers
Container view controllers have two responsibilities in the unwind process, both discussed below. If you are using the container view controllers provided by the SDK, such as UINavigationController, these are handled automatically.
If you were to create a simple container view controller to act as the parent for your three view controllers, you could use its viewControllerForUnwindSegueAction method to check each of its child controllers for the existence of the unwind action, before calling canPerformUnwindSegueAction on that controller, and finally returning the first one of those which returns YES.
Selecting a Child View Controller to Handle An Unwind Action
As mentioned in How an Unwind Segue Selects its Destination, the source view controller of an unwind segue defers to its parent to locate a view controller that wants to handle the unwind action. Your container view controller should override the method shown in Listing 2 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.
A container view controller should search its children in a way that makes sense for that container. For example, UINavigationController searches backwards through its viewControllers array, starting from the view at the top of the navigation stack.
Listing 2 Override this method to return a view controller that wants to handle the unwind action.
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
Container view controller design has a whole article dedicated to it by Apple, which I won't duplicate here (more than enough of Apple's writing in this answer already!) but it looks like it will take some thought to get it right, as it depends on the exact role you want these to play in your application.
A quick workaround, to get your desired behaviour using unwind segues, could be to embed the view controllers in a UINavigationController, and then hide the navigation bar using
[self.navigationController setNavigationBarHidden:YES];

Josh's answer led me to a solution. Here's how to accomplish this:
Create a root UINavigationController, and assign it to a class that extends UINavigationController and overrides the segueForUnwindingToViewController:fromViewController:identifier method. This could be filtered by the identifier if desired.
CustomNavigationController:
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier {
return [[CustomUnwindSegue alloc] initWithIdentifier:identifier source:fromViewController destination:toViewController];
}
Create a custom push segue, that behaves like a modal segue, but utilizes our navigation controller. Use this for all "push" segues.
CustomPushSegue:
-(void) perform{
[[self.sourceViewController navigationController] pushViewController:self.destinationViewController animated:NO];
}
Create a custom unwind segue, that uses the navigation controller to pop to the destination. This is called by our navigation controller in the segueForUnwindingToViewController:fromViewController:identifier method.
CustomUnwindSegue:
- (void)perform {
[[self.destinationViewController navigationController] popToViewController:self.destinationViewController animated:NO];
}
By utilizing a combination of these patterns, the second view controller never appears during the unwind process.
New log output:
one did appear
#### segue to two
two did appear
#### segue to three
three did appear
#### unwind to one
one did appear
I hope this helps someone else with the same issue.

Looks like custom segues are being used, so it's possible that's interfering somehow, although I've never seen it happn. I suggest you check out Apple's example project. It also has custom segues in it so it should serve as a good starting point for you.
Apple's Unwind Segue Example

I surprised you're seeing the behavior you're seeing, but one way to change it would be to use an explicit dismiss rather than unwind segues (this assumes the forward segues are modal).
Everything will look right if VC1 does this:
[self dismissViewControllerAnimated:YES completion:^{}];
Or if some other vc does this:
[vc1 dismissViewControllerAnimated:YES completion:^{}];
The only hitch is that you'll need a handle to vc1 if you want to dismiss from some other vc. You could use a delegate pattern to let vc1 know it should do the dismiss, but a simpler solution is to have vc2 or 3 post a notification when the unwind should happen.
VCs 2 or 3 can do this:
// whenever we want to dismiss
[[NSNotificationCenter defaultCenter] postNotificationName:#"TimeToDismiss" object:self];
And VC1 can do this:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(doDismiss:)
name:#"TimeToDismiss"
object:nil];
- (void)doDismiss:(NSNotification *)notification {
[self dismissViewControllerAnimated:YES completion:^{}];
}

Related

unwind segue / performSegueWithIdentifier: problems

in my app, there is a chatting service. when the user wants to message someone new, he press on "new chat" and a Table View Controller is shown to select from the list of friends. i'm using unwind segues in order to go back to the previous view controller and share the data between the two view controllers (mainly the data will be the friend to have a chat with). the unwind segue works perfectly, however in my app, when the user goes back to the main VC, i fire a new segue in order to go to another VC directly where the user has the chat; and this isn't working. All segues are well connected and i tried NsLog in every corner and it's entering there and even the prepareForSegue: is being accessed. i tried putting an NsTimer, thought there might be some technical conflict, and it didn't work. i change the segue to the chat VC to a modal and now it's giving me this error:
Warning: Attempt to present on whose view is not in the window hierarchy!
i can access this VC in other ways and it's in the hierarchy. my questions are: what could be wrong? does unwind segues alter the windows hierarchies ?
PICTURE:
to present the problem more visually, the main VC i'm talking about is on the bottom left connected to a navigation view controller. when a user presses new chat, the two VC on the top right are presented (one to choose between friends or group, the other to show the friends/ groups). so when a friend is selected for say from the top right VC i should unwind segue to the main VC. as you can see from the main VC there is other segues. non of them can work if i do unwind segue and they do work if i operate normally.
The reason it is not working and is giving you that error is because things arent happening in the order you think they are.
When the unwind happens the view which is visible is not dismissed yet, therefore you are trying to perform a segue on a view which is in fact not in that hierarchy like the error says, take this for example, placing NSLog statements in the final view and then in the unwind method in your main view controller you can see the following:
2013-11-27 14:51:10.848 testUnwindProj[2216:70b] Unwind
2013-11-27 14:51:10.849 testUnwindProj[2216:70b] Will Appear
2013-11-27 14:51:11.361 testUnwindProj[2216:70b] View Did Disappear
Thus the unwind in the main view is getting called, the view will appear (your main view controller), and then your visible view is dismissed. This could be a simple fix:
#interface ViewController () {
BOOL _unwindExecuted;
}
#end
#implementation ViewController
- (void)viewWillAppear:(BOOL)animated
{
NSLog(#"Will Appear");
if (_unwindExecuted) {
_unwindExecuted = NO;
[self performSegueWithIdentifier:#"afterunwind" sender:self];
}
}
- (IBAction)unwind:(UIStoryboardSegue *)segue
{
NSLog(#"Unwind");
_unwindExecuted = YES;
}
Don't use timers or delays to try and anticipate when a view may exist.
Instead, use calls like: - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
The completion block will let you know when you've arrived back at the main VC. Alternatively, look at the various calls associated with segues so that you know precisely when you can perform operations on the new window.
If all else fails, there's always UIViewController viewDidAppear.
This is a common problem for the view controller that is handling the unwinding, because during unwind, that view controller is likely to not be in the window hierarchy.
To solve, I added a property segueIdentifierToUnwindTo to coordinate the unwinding.
This is similar to the answer by JAManfredi, but extending it to be able to segue to any view controllers.
#interface FirstViewController ()
// Use this to coordinate unwind
#property (nonatomic, strong) NSString *segueIdentifierToUnwindTo;
#end
#implementation FirstViewController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Handle unwind
if (_segueIdentifierToUnwindTo) {
[self performSegueWithIdentifier:_segueIdentifierToUnwindTo sender:self];
self.segueIdentifierToUnwindTo = nil; // reset
return;
}
// Any other code..
}
// Example of an unwind segue "gotoLogin"
- (IBAction)gotoLogin:(UIStoryboardSegue*)sender {
// Don't perform segue directly here, because in unwind, this view is not in window hierarchy yet!
// [self performSegueWithIdentifier:#"Login" sender:self];
self.segueIdentifierToUnwindTo = #"Login";
}
#end
I also shared how I use unwind segues with a FirstViewController on my blog: http://samwize.com/2015/04/23/guide-to-using-unwind-segues/
okay solved this, the problem is that the view wasn't there yet and i had to delay the process by this:
[self performSelector:#selector(fireNewConv) withObject:nil afterDelay:1];
However, something interesting is that i tried delaying by NSTimer but it didn't work.

How to select the right parent view controller

I have a child view controller called by two view controllers placed in different places in the storyboard. In this child there is a button to close the actual view, i want to assign an IBAction or a Segue to connect this view with is real parent view controller.
Maybe i'm wrong but it's possible to do this with an Unwind Segue?? This is the first time i have to use those, somebody could help me please?
Thanks a lot! Peace
What's the relationship between the parent and the child? Is it pushed on the navigation controller or presented (modally)? Then you can use in the case of UINavigationController
- (UIViewController *)popViewControllerAnimated:(BOOL)animated; // Returns the popped controller.
or in case of presented (modally) viewcontroller
- (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^)(void))completion NS_AVAILABLE_IOS(5_0);
You can also use unwinding segues of course, check out this tutorial.
No matter the viewController you're using, you can access its parent by calling :
// < iOS 5
[myViewController parentViewController]
// >= iOS 5
[myViewController presentingViewController]
After getting it you'll just have to check which one it :
if ([[myViewController presentingViewController] isKindOfClass:[myParentClassA class]])
{
// do anything
}
It is possible to create unwind methods with the same name in both parent view controllers:
- (IBAction)unwindToThisViewController:(UIStoryboardSegue *)segue {
}
The unwind mechanism automatically will walk through the responder chain and find the correct parent view controller

-segueForUnwindingToViewController: fromViewController: identifier: not being called

I have created a custom segue that presents a view controller inside a container that is very similar with Apple's own modal view controllers (I've implemented it as a UIViewController subclass).
I'm now trying to create a custom unwind segue but there's no way I can get the method -segueForUnwindingToViewController: fromViewController: identifier: to be called.
I've also implemented -viewControllerForUnwindSegueAction: fromViewController: withSender: on my container so I can point to the correct view controller (the one that presented this modal) but then the method that should be asked for my custom unwind segue doesn't get called anywhere.
Right now, the only way for me to dismiss this modal is to do it on the -returned: method.
Did anyone could successfully do this with a custom unwind segue?
EDIT:
A little bit more code and context
My unwind view controller is configured in the storyboard, not programatically.
I have these pieces of code related to the unwind segues in my controllers:
PresenterViewController.m
I'm using a custom method to dismiss my custom modals here (-dismissModalViewControllerWithCompletionBlock:).
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController
fromViewController:(UIViewController *)fromViewController
identifier:(NSString *)identifier {
return [[MyModalUnwindSegue alloc] initWithIdentifier:identifier
source:fromViewController
destination:toViewController];
}
-(IBAction)returned:(UIStoryboardSegue *)segue {
if ([segue.identifier isEqualToString:#"InfoUnwindSegue"]) {
[self dismissModalViewControllerWithCompletionBlock:^{}];
}
}
MyModalViewController.m
Here I only use -viewControllerForUnwindSegueAction: fromViewController: withSender: to point to the view controller that I should be unwind to.
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action
fromViewController:(UIViewController *)fromViewController
withSender:(id)sender {
return self.myPresentingViewController;
}
The behavior I was expecting was that MyModalViewController was called to point to the view controller that should handle the unwinding and then this view controller had his -segueForUnwindingToViewController: fromViewController: identifier: method called before -returned: gets called.
Right now -segueForUnwindingToViewController: fromViewController: identifier: never gets called.
I must also say that I already tried different configurations. Everywhere I put my method to return the unwind segue it never gets called. I've read that I can subclass a navigation controller and then it gets called but I don't know how it would fit in my solution.
EDIT 2: Additional info
I've checked that MyModalViewController has his -segueForUnwindingToViewController: fromViewController: identifier: method called when I want to dismiss a regular modal view controller presented by it. This may be because he's the top most UIViewController in the hierarchy.
After checking this I've subclassed UINavigationController and used this subclass instead to contain my PresenterViewController. I was quite surprised to notice that his -segueForUnwindingToViewController: fromViewController: identifier: method is called as well.
I believe that only view controllers that serve as containers have this method called. That's something that makes little sense for me as they are not the only ones presenting other view controllers, their children are also doing so.
It's not OK for me to create logic in this subclass to choose which segue class to use as this class has no knowledge of what their children did.
Apple forums are down for the moment so no way to get their support right now. If anyone has any more info on how this works please help! I guess the lack of documentation for this is a good indicator of how unstable this still is.
To add to the answer from #Jeremy, I got unwinding from a modal UIViewController to a UIViewController contained within a UINavigationController to work properly (I.e how I expected it to) using the following within my UINavigationController subclass.
// Pass to the top-most UIViewController on the stack.
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)
toViewController fromViewController:(UIViewController *)
fromViewController identifier:(NSString *)identifier {
UIViewController *controller = self.topViewController;
return [controller segueForUnwindingToViewController:toViewController
fromViewController:fromViewController
identifier:identifier];
}
.. and then implementing the segueForUnwindingToViewController as usual in the actual ViewController inside the UINavigationController.
This method should be declared on the parent controller. So if you're using a Navigation Controller with a custom segue, subclass UINavigationController and define this method on it. If you would rather define it on one of the UINavigationController's child views, you can override canPerformUnwindSegueAction:fromViewController:withSender on the UINavigationController to have it search the children for a handler.
If you're using an embedded view (container view), then define it on the parent view controller.
See the last 10 minutes of WWDC 2012 Session 407 - Adopting Storyboards in Your App to understand why this works!
If you're using a UINavigationController and your segue is calling pushViewController then in order to use a custom unwind segue you'll need to subclass UINavigationController and override - (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier.
Say I have a custom unwind segue called CloseDoorSegue. My UINavigationController subclass implementation might look something like:
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier {
UIStoryboardSegue* theSegue;
if ([identifier isEqualToString:#"CloseDoor"]) {
theSegue = [CloseBookSegue segueWithIdentifier:identifier source:fromViewController destination:toViewController performHandler:^(void){}];
} else {
theSegue = [super segueForUnwindingToViewController:toViewController fromViewController:fromViewController identifier:identifier];
}
return theSegue;
}
Set your UINavigationController subclass as the navigation controller class in the storyboard. You should be good to go provided you have setup the Exit event correctly with "CloseDoor" as the identifier. Also be sure to call 'popViewControllerAnimated' in your unwind segue instead of dismiss to keep in line with UINavigationControllers push/pop mechanism.
iOS development Library
There is a discussion on iOS development Library along with this method.
- segueForUnwindingToViewController:fromViewController:identifier:
Make sure your MyModalViewController is the container role rather than a subcontroller of a container. If there is something like [anotherVC addChildViewController:myModalViewController];,you should put the segueForUnwindingToViewController method in some kind of "AnotherVC.m" file.
Discussion
If you implement a custom container view controller that
also uses segue unwinding, you must override this method. Your method
implementation should instantiate and return a custom segue object
that performs whatever animation and other steps that are necessary to
unwind the view controllers.

iOS + Storyboard: Re-using ViewControllers in navigation

in my current application, I want to use a certain UICollectionView several times, but with different selection behaviours. Consider the following storyboard layout as "is" (and working):
Tab Bar Controller (2 items):
-> Navigation Controller 1 -> Collection View Controller -> some Table View Controller
-> Navigation Controller 2 -> (Basic) View Controller
The Basic View Controller has two UIButtons which have Storyboard Push-connections to the Collection View Controller. What I want is to transition from the Basic View Controller to the Collection View Controller, but selecting an item from the collection should pop the view and return to the Basic View Controller.
I have set a custom property in the Collection View Controller which gets set in the corresponding prepareForSegue message of the Basic View Controller (or not at all, if the user selects a Tab Bar Item), so there's no problem in detecting which controller or which UI component triggered the push (there are 3 ways: selecting the tab bar item or tapping one of the buttons on Basic View).
The problem is popping the Collection View.
My code so far:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ( self.mode == nil ) {
// do nothing
} else if ( [self.mode isEqualToString:#"foobar"] ) {
// one way I tried
[self dismissViewControllerAnimated:YES completion:nil];
} else if ( [self.mode isEqualToString:#"blah"] ) {
// other method
BasicViewController *targetVC = self.navigationController.viewControllers[ 0 ];
[self.navigationController popToViewController:targetVC animated:YES];
}
}
Unfortunately, my app crashes in the lines dismiss resp. popToViewController. Is it even possible to use the same view controllers in different ways of navigation?
I hope it's enough information to help me out on this one. As you might know, projects grow, and I don't know if there's more code to consider :)
Thanks in advance!
The prepareForSegue: method is not the right place to put that code. It's called right before the segue is performed, which usually means there's allready some kind of transition about to happen.
I'm assuming you've connected your collection view cells with a segue and are now trying to modify the behaviour of that segue depending on the viewcontroller 'before' the collectionVC.
In that case there are some possible solutions:
don't use segues for that specific transition and do any vc-transitions manually
write your own UIStoryboardSegue subclass that can handle the different transitions. See UIStoryboardSegue Class Reference. Then you can set some property of your custom segue in the prepareForSegue: method, to tell the segue which transition it should perform.
use unwinding segues. See What are Unwind segues for and how to use them?
The action of a selected a row in your 'Collection View Controller' differs depending on from where the 'Collection View Controller' was presented. In one case, return to 'Basic View Controller' and in the other segue to 'some Table View Controller'
Implement this by defining tableView:didSelectRowAtIndexPath: with something like:
- (void) tableView: (UITableView *) tableView
didSelectRowAtIndexPath: (NSIndexPath *) indexPath
{
if (self.parentIsBasicViewController) // set in the incoming segue, probably.
[self.presentingviewController dismissViewControllerAnimacted: YES completion: nil]
else
[self performSegueWithIdentifier: #"some Table View Controller" sender: self];
}

Can't pop from an unwind segue action

I've got a tableview based navigation controller. The root view controller has a + button which adds a new item. This results in a push segue to the view controller which allows the user to enter the data for the new item.
On this view controller, the upper left navbar item is the standard labeled "back" button. I've placed a "Save" [bar] button [item] as the upper right navbar item.
I created an unwind IBAction pushed view controller.
I then control-dragged from the Save button to the exit on the view controller and wired it to the unwind IBAction.
This unwind action is getting called when I hit the Save button.
However, nothing I do in the unwind action pops the view controller. Here is a little code:
- (BOOL)canPerformUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
return YES;
}
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
NSLog( #"Should seque from %#", identifier );
return YES;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
}
- (IBAction)unwindFromGoalEdit:(UIStoryboardSegue *)segue {
NSLog( #"UNWINDING!!");
[self.navigationController popToViewController:segue.destinationViewController animated:YES];
}
In the unwind action below, I've attempted various ways to pop the view controller. But nothing happens. No runtime error, nothing. I do however see the NSLog() so I know it's being called.
Thoughts?
The problem is probably that you have a retain cycle. The unwind automatically pops every view above it unless it can't. You do not pop anything from the unwind yourself. So, add a log to the view controller you're expecting to be popped in the dealloc method, or a break point. If that doesn't fire, then you know the view controller is not being popped. This almost certainly means you have a retain cycle. Look at your delegates or anywhere you are passing self (the parent) into a block. The top view controller should not have a strong reference to it's parent. Actually, no child should ever have a strong reference to its parent for this very reason. If it does the unwind will not work.
Which class is unwindFromGoalEdit: in? It looks like you may have added it to the "new item" view controller... This method should be placed in the root view controller, and there is no need to call the popToViewController. An empty method should suffice here.

Resources