12
votes

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:

enter image description here

4
Can you show a screenshot of your storyboard. An unwind segue shouldn't show the intermediate views. How did you define the "OneSegue" in the storyboard?Fogmeister
I've added a screenshot. The unwind segue was created by control-dragging from the Three View Controller icon to the Exit icon, and selecting "unwindToOne".Craig
Hmm... that's correct. You seem to have set it up correctly. What transitions are you using for SegueTwo and SegueThree? Type of Segue.Fogmeister
I'm pretty sure this is a bug in iOS 8. I've tested this with going back multiple controllers, and viewDidAppear is called for all of them, but only the controller next after the one you're going back to shows briefly. This did not happen in iOS 7.rdelmar
I am seeing the exact same behavior with iOS 11 and Xcode 9.3. Segue from A to B, then B to C. Unwinding from C to A shows a brief flash of B. I worked around it by bringing a neutral background view to the foreground in B.viewDidDisappear(). This only works, of course, if the neutral view blends in with A's background.Andrew Duncan

4 Answers

6
votes

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];
4
votes

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.

0
votes

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

0
votes

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