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.
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.
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:
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
Hi Zack, I updated the post with sample code in Swift.
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
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
I found a workaround which I have described here: http://stackoverflow.com/questions/28565237/ios-8-bug-with-dismissviewcontrolleranimated-completion-animation/29734491#29734491
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
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
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
Could I have the source code sample of it?
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.
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.
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?
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.
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
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
Hey Mike,
Coul you please share the project files some how?
Cheers!
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?