Reputation: 691
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:
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:
Upvotes: 12
Views: 5467
Reputation: 691
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:
I hope this helps someone else with the same issue.
Upvotes: 4
Reputation: 3947
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];
Upvotes: 6
Reputation: 62676
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:^{}];
}
Upvotes: 0
Reputation: 11341
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.
Upvotes: 0