18 Comments

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: