Can't pop from an unwind segue action - ios

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.

Related

How to unwind through multiple views without displaying intermediate views

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:^{}];
}

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

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];
}

implement 2 cancel button in a segue

I have a view controller with 2 different segues that goes to 2 different view controller,and i have to implement the cancel button in both the controllers.When i press the cancel button,in both the controller,the view will return to the initial view controller.My question is how can i implement the buttons?When i try with this code the compiler warning:Multiple declaration of method "cancel:" found and ignored.Thank you.
interface:
-(IBAction)cancel:(UIStoryboardSegue *)segue;
-(IBAction)done:(UIStoryboardSegue *)segue;
-(IBAction)cancel:(UIStoryboardSegue *)segue;
implementation:
-(IBAction)done:(UIStoryboardSegue *)segue
{
if([[segue identifier] isEqualToString:#"ReturnInput"]){
AddSightingViewController *addController = [segue sourceViewController];
if (addController.birdSighting) {
[self.dataController
addBirdSightingWithSighting:addController.birdSighting];
[[self tableView]reloadData];
}
[self dismissViewControllerAnimated:YES completion:NULL];
}
}
-(IBAction)cancel:(UIStoryboardSegue *)segue
{
if([[segue identifier] isEqualToString:#"CancelInput"]){
[self dismissViewControllerAnimated:YES completion:NULL];
}
}
I'm not sure of what you're trying to do. But I think the cancel method needs to be in the 2 child View Controllers, not in the main one. One for each controller (and one cancel button for each view). That way you won't have any problems with multiple declarations of a method.
From your code I conclude that you are using (or want use) exit segues for canceling.
First, you should only have one method declaration and implementation for your cancel method in the initial view controller. In your storyboard create exit segues by control-dragging from your cancel buttons to the green exit icon blow the view controller and select the cancel method defined in the initial view controller. Do that for both view controllers. You should also give your exit segues different identifiers in your storyboard (you need to select the segue in the Document Outline to change its identifier).
Then your cancel method in your initial view controller can look something like this:
-(IBAction)cancel:(UIStoryboardSegue *)segue
{
if([[segue identifier] isEqualToString:#"CancelInput1"]) {
// Do something
} else if([[segue identifier] isEqualToString:#"CancelInput2"]) {
// Do something different
}
}
If you don't want to do anything when canceling just leave the method empty.
If you wan't to go back you need to implement an unwind segue.
To do this, define a method to go back on the original view controller (the one you want to go back). You can leave the method empty.
- (IBAction)methodName:(UIStoryboardSegue *)segue
{
}
Then on IB ctrl + drag from the button (or from the view controller) to the green "exit" icon. Select the methodName from the popup menu. If you did it from the view controller set the identifier on the segue and call it with performSegueWithIdentifier: from the button action.
Considerations:
The method name will be detected in every view controller on the storyboard.
You can define the same method name in different view controllers, but when you execute the unwind segue, you will go back to the most recent on the navigation path.

Resources