Custom Animation for an Unwind Segue

On the first post in this series, someone left a comment asking, “What do you do if you want a custom segue transition for the unwind?” I thought that was a great topic to cover since most people only worry about the transitions going forward on a navigation stack and don’t think about how to transition when you unwind several layers back.

Unwind example

We are going to build upon the example we build in my first post.  In that example, I put a “Next” button on each view controller that goes from #1 to #3 in order. Then I created an Unwind Segue that goes from #3 back to #1.

Trying UIViewControllerTransitioningDelegate

Introduced in iOS 7, the UIViewControllerTransitioningDelegate is the new way to do custom animations between view controllers. I’ve used this before, when I wanted to use a custom transition, but I never tried it for an Unwind Segue. When the reader asked the question of how to do a custom animation for an Unwind Segue, my first thought was to use the transitioning delegate.

Unfortunately, it didn’t work. Even though I set the transitioning delegate, the delegate functions were never called. There are two unanswered questions on Stack Overflow that demonstrate the problem I had: #1 and #2.

Specifing a Custom Segue

The solution was to use a custom segue for the unwind. Unfortunately you cannot specify a custom segue for an unwind in interface builder like you can on a normal segue. You have to specify it with code.

There is a function on UIViewController that you can override called segueForUnwindingToViewController which specifies which segue to use for the unwind. The tricky part is knowing which view controller to override this function on. The answer is to do it on the parent of the view controllers you are transitioning to/from. In my case it is the UINavigationController. The navigation controller is the container view controller for the first and third view controller. If I had embedded child view controllers in my views, then the answer would not be the navigation controller but the parent view controller.

Create a custom navigation controller by inheriting from UINavigationController. Then make sure to set the custom class of your navigation controller in interface builder. Now you can override the segueForUnwindingToViewController function. Here is a minimal implementation.


class MyNavigationController: UINavigationController {
override func segueForUnwindingToViewController(toViewController: UIViewController,
fromViewController: UIViewController,
identifier: String?) -> UIStoryboardSegue {
return UIStoryboardSegue(identifier: identifier, source: fromViewController, destination: toViewController)
}
}



#import "MyNavigationController.h"
@implementation MyNavigationController
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier {
return [UIStoryboardSegue segueWithIdentifier:identifier source:fromViewController destination:toViewController performHandler:nil];
}
@end

Adding Animation to your Custom Segue

You can create custom segue class that is inherited from UIStoryboardSegue and return an instance from the segueForUnwindingToViewController. Inside your custom segue, you need to override the perform function to do your animation. There is an easier way than having to create a custom segue class. In Swift, they provide a convenience initializer on UIStoryboardSegue with a tail closure as the last parameter. The closure is called when the perform function normally would be called on a segue. In Objective-C, you can use the class method on UIStoryboardSegue that returns a Segue with a block that executes when the perform function normally would. Now I can do all of my animation within the block/closure. Here is the complete implementation of the segueForUnwindingToViewController function.


class MyNavigationController: UINavigationController {
override func segueForUnwindingToViewController(toViewController: UIViewController,
fromViewController: UIViewController,
identifier: String?) -> UIStoryboardSegue {
return UIStoryboardSegue(identifier: identifier, source: fromViewController, destination: toViewController) {
let fromView = fromViewController.view
let toView = toViewController.view
if let containerView = fromView.superview {
let initialFrame = fromView.frame
var offscreenRect = initialFrame
offscreenRect.origin.x -= CGRectGetWidth(initialFrame)
toView.frame = offscreenRect
containerView.addSubview(toView)
// Being explicit with the types NSTimeInterval and CGFloat are important
// otherwise the swift compiler will complain
let duration: NSTimeInterval = 1.0
let delay: NSTimeInterval = 0.0
let options = UIViewAnimationOptions.CurveEaseInOut
let damping: CGFloat = 0.5
let velocity: CGFloat = 4.0
UIView.animateWithDuration(duration, delay: delay, usingSpringWithDamping: damping,
initialSpringVelocity: velocity, options: options, animations: {
toView.frame = initialFrame
}, completion: { finished in
toView.removeFromSuperview()
if let navController = toViewController.navigationController {
navController.popToViewController(toViewController, animated: false)
}
})
}
}
}
}


#import "MyNavigationController.h"
@implementation MyNavigationController
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier {
return [UIStoryboardSegue segueWithIdentifier:identifier source:fromViewController destination:toViewController performHandler:^{
UIView *fromView = fromViewController.view;
UIView *toView = toViewController.view;
UIView *containerView = fromView.superview;
NSTimeInterval duration = 1.0;
CGRect initialFrame = fromView.frame;
CGRect offscreenRect = initialFrame;
offscreenRect.origin.x -= CGRectGetWidth(initialFrame);
toView.frame = offscreenRect;
[containerView addSubview:toView];
// Animate the view onscreen
[UIView animateWithDuration:duration
delay:0
usingSpringWithDamping:0.5
initialSpringVelocity:4.0
options:0
animations: ^{
toView.frame = initialFrame;
} completion: ^(BOOL finished) {
[toView removeFromSuperview];
[toViewController.navigationController popToViewController:toViewController animated:NO];
}];
}];
}
@end

You will need to beef up this code by doing checks on the identifier to be sure you are doing the right animation with the correct segue. After you hit the “home” button, the animation bounces the view controller #1 back into view.

The final result

You can animate the two views in many different ways. Do whatever makes sense for your application. The key is in the completion block of the animation you need to do a popToViewController with the animated flag set to false/NO. This will do the unwind after the animation completes. 

Do you have any other questions about Unwind Segues?


This is the third part of my series on Unwind Segues:

Conversation
  • Zack Shapiro says:

    Mike, thanks for explaining this. Really appreciate it. Would you mind doing the animation code in Swift too? This has been referenced by a number of Stack Overflow posts, it would be really helpful to have this in Swift too, IMO

    • Mike Woelmer Mike Woelmer says:

      Hi Zack, I updated the post with sample code in Swift.

  • Sandy says:

    Hi Mike,

    Thanks for the great posts. I’ve used them to implement segues in my app.

    I have 3 view controllers A –> B –>C. “C” unwinds directly back to “A”. What I am seeing is that while unwinding from “C to B”, controller “B” is being “woken up” and then dismissed. That is, viewWIllAppear, viewDidAppear, etc, of B are being invoked. This is all very quick, but causes a “flash” just before “A” is displayed.

    Do you have any ideas on how to fix this? That is, “B” should not be displayed while unwinding from “C to A’. I have tried custom unwind segues with no animation but cannot avoid the visual flash.

    Thanks,
    – S

    • Sebastian says:

      Hi,

      I am seeing the exact same issue. Building the app with iOS7 base SDK it works on iOS7 and 8. But when building it with iOS8, it works on iOS7, but on iOS8 I have this issue, too. I have been fiddling around a lot already, but the intermediate view still flashs. I started debugging it with quicktime (making a screen cast, to see what flashes there and get a better understanding). But I did not make it work yet. If anyone knows, it would be great to help me out.

      Thanks, Sebastian

    • Mike Woelmer Mike Woelmer says:

      I tried to reproduce the problem you mentioned and I was not able to. I placed a viewWillAppear function in my 2nd view controller and it never got called when I transitioned from 3 to 1. I also never saw the visual artifact you mentioned of seeing the second view controller. I would double check that you have your unwind handler in the correct class. It should be defined in your A class. It sounds like the system is unwinding to B instead of A.

      Best of luck finding your problem
      –Mike

      • Sebastian says:

        Thanks for checking it again. Currently I guess that the bug depends on the way how to add the viewController: in your sample you do it with navController.popToViewController. My implementation uses dismissViewControllerAnimated. I will stick with the workaround for now. Thanks

      • Sandy says:

        Mike, thanks for the response. Yeah, I checked that C unwinds directly back to the handler in A. My assumption is that something is popping view controllers off of some stack to get from C to A. Oh well.

  • Great posts Mike – here’s an idea for a fourth in the series(if this is possible) – how can you get a call to prepareForUnwind or a similar unwind method when hitting the Navigation Bar’s back button? Here’s a Stack Overflow which discusses the problem, appears as though there may not be a way(without implementing hacks like overriding the back button or just overriding the viewWillDisappear method etc ): http://stackoverflow.com/questions/13551778/unwind-segue-with-navigation-back-button

  • Alex Chan says:

    Could I have the source code sample of it?

  • Murray Sagal says:

    I’m not sure when this functionality was added to Xcode but with Xcode 7 it is possible to specify a UIStoryboardSegue subclass for an unwind segue.

    1. Create the unwind segue in the usual manner.
    2. Select the unwind segue in the storyboard outline.
    3. In the Class popup, select your custom segue.

    • Mike Woelmer Mike Woelmer says:

      You are correct. Before Xcode 7, the only options in attributes inspector that you could change was the identifier and the action. Now they let you specify the class, module, and a checkbox for animation. Perhaps that checkbox gives you the container for the animation.

  • Murray Sagal says:

    The thing I like about the new method is that you are handed a handy container to use for the animations. I see in your code that you use fromViewController.view.superview. Is that a best practise? Or are there other factors that determine the choice?

    • Mike Woelmer Mike Woelmer says:

      I would say it is a best practice. It is the more general choice since you want the parent of the transitioning views. This is not always the navigation controller although in my example it is.

  • Zoej says:

    I follow this post to do custom segue in navigation controllers.When I want to make an unwind segue ,it does not work in my project. I create a subclass of UINavigationController ,and override the func segueForUnwindingToViewController , and set it in my storyboard ,but the func do not called ,Thank you for you reply

    • Zoej says:

      I just find out that the back button of left top in my view controller, can not call my custom segue, only by write some other actions to call dismiss func

  • O Lima says:

    Hey Mike,

    Coul you please share the project files some how?

    Cheers!

  • DC says:

    Hi Mike,

    Thanks a bunch for this post!

    I realized something strange thought in that when I implemented the above code block, it would execute the segue animation but then in the completion block, cause the target “to view” to disappear.

    Upon inspecting, I saw that you put ‘toView.removeFromSuperview()’ in the completion. Why would we want to the toView to be removed? I changed it to ‘fromView.removeFromSuperview()’ and it was all good.

    Was this written in error?

  • Comments are closed.