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
Related
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:^{}];
}
I have the following storyboard in an application I am working on:
At the root, I have a Tab Bar Controller. It links to two View Controllers.
The first View Controller to display a newsfeed with pictures uploaded by the user (the one at the bottom in the storyboard).
The second View Controller serves to initiate the taking of a picture and attach some data to it. In the last step (top right), when touching "Save" in the right item of the Navigation bar, I want the user to be redirected to the newsfeed View Controller passing it some data.
I tried using a segue and it works. The data are passed to the newsfeed but the wrong tab is selected. I changed the selected tab using
[self.tabBarController setSelectedIndex:0];
But by tapping on the second tab again, things are messed up. I can see the newsfeed instead of the taking a picture screen. If I tap again, it crashes.
At some point I thought I may have got the wrong storyboard and should have implemented a TabBar in my newsfeed and handle the taking picture as a modal view.
Would you know any clean way to achieve this?
Thanks
You should not use a normal segue, which adds the destination controller to the stack. To do what you are trying to the best way should be to use an unwind segue. This is a rough sketch of what you need to do:
• Declare an unwind segue action in the NewsfeedController like (IBAction)unwindFromPictureSaved:(UIStoryboardSegue *)segue;
• Connect your "Save" button in your SavingPictureController to the "Exit" icon in the storyboard and select the previously defined method;
• In the newly created unwind segue define its identifier with something like SavedPictureSegue;
• Define the data to be passed in SavingPictureController's header with something like #property (strong, readonly, nonatomic) id passedData;
• In SavingPictureController implement
-(void)prepareForSegue:(UIStoryboardSegue *)segue
{
if ([segue.identifier isEqualToString:#"SavedPictureSegue"]) {
_passedData = // Your data here
}
}
• In NewsfeedController now implement the previously defined method and fetch the data from (SavingPictureController *)segue.sourceController. Be sure to #import "SavingPictureController.h".
Thanks to #Davide, I created a subclass of TabBarController and implemented the method below:
// Find the appropriate controller to answer to an unwind segue
// For each child view controller
// Checks if it is a Navigation Controller
// If it is check its children view controllers
// Return the first view controller that answers the unwind segue
// This because I assumed the default behavior is just to check one level up (in this case, it would have stopped at the NavigationController)
// Based on https://developer.apple.com/library/ios/technotes/tn2298/_index.html#//apple_ref/doc/uid/DTS40013591-CH1-CCVC-SELECTING_A_CHILD_VIEW_CONTROLLER_TO_HANDLE_AN_UNWIND_ACTION
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
BOOL resChildren, res;
for(UIViewController *controller in self.childViewControllers) {
if ([controller isKindOfClass:[UINavigationController class]]) {
for (UIViewController *childController in controller.childViewControllers) {
resChildren = [childController canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
if (resChildren) {
return childController;
}
}
}
res = [controller canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
if (res) {
return controller;
}
}
return nil;
}
Then in the unwind method of the 'NewsFeedController" it is necessary to set the correct index to see the controller with something like:
[self.tabBarController setSelectedIndex:1];
I uploaded a demo on github at https://github.com/kintso/unwindSegueWithTabBarControllerAndNavigationController
I'm currently working on a project which implements a custom navigation controller, whose code is here:
https://gist.github.com/emilevictor/724a6602fedb8100650c
In one of my controllers, which gets pushed to the navigationController via a push segue, I have an action on a button to return to the main screen:
- (IBAction)returnToMainScreen:(id)sender
{
NSArray *returnedControllers = [self.navigationController popToRootViewControllerAnimated:YES];
NSLog(#"Popped to root view controller.");
}
This will return the current view controller and one preceding it in the returnedControllers array.
However, it doesn't change screens, or call any viewDidDisappear functions. Anyone know why?
Make sure you child viewControllers are being added to the parent viewController with the method [UIViewController addChildViewController:]
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.
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.