Custom unwind segue animation

2020-05-21 10:14发布

问题:

I have the following set up:

The red controller has a method

- (IBAction)unwindToRed:(UISegue *)segue

and a

- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier

which returns a custom segue for this event.

In my custom segue I have the following in the perform code:

- (void)perform
{
    // Just helpers to get the controllers from the segue
    UIViewController *destination = self.destinationViewController;
    UIViewController *source = self.sourceViewController;

    // Setting transitioning delegate for custom transitions
    destination.transitioningDelegate = self;
    source.transitioningDelegate = self;

    destination.modalTransitionStyle = UIModalPresentationCustom;
    source.modalPresentationStyle = UIModalPresentationCurrentContext;

    // When I am transitioning to a new one
    if(!self.reversed)
    {
        [source presentViewController:destination animated:YES completion:nil];
    }
    // Or reversing from a current one
    else
    {
        [source dismissViewControllerAnimated:YES completion:nil];
    }
}

As you can see I have setup a transition delegate which which in turn calls

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext

on an animation controller where I apply my custom animation. But now when I tap Unwind to Red on the yellow view, it only "unwinds" to its parent. (e.g the blue one).

When I leave the custom segue out of the equation it works just fine. It goes al the way to the top.

Now I think this has to do with the

[source dismissViewControllerAnimated:YES];

line, since that usually just goes "one up" my question is I think, what should I call there so it goes all the way up to red when needed? Yellow is a modal overlay. So "pop too root" would not work.

回答1:

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)
    }
}

Adding Animation to Custom Segue

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)
                    }
                })
            }
        }
    }

Details are from this post



回答2:

You want to use the Navigation controller's popToViewController:

 [source.navigationController popToViewController:destination animated:YES];

or, use animated:NO and put the whole thing in an animation block: e.g.:

UIViewAnimationOptions animation  = UIViewAnimationOptionTransitionFlipFromRight | UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowAnimatedContent;

UIViewController *source = (UIViewController *) self.sourceViewController;
UIViewController *dest = (UIViewController *) self.destinationViewController;


[UIView transitionWithView:src.navigationController.view duration:0.4
                   options: animation
                animations:^{
                    [source.navigationController popToViewController:dest animated:NO];
                }
                completion:^(BOOL finished) {
                }];


回答3:

  1. Check your UIStoryboardSegue implementation. Was your use of modalTransitionStyle a typo in your post or in your actual code? Corrected:

    destination.modalPresentationStyle = UIModalPresentationCustom;
    
  2. I have also had success without writing a custom UIStoryboardSegue. Try linking an "unwind to Red" button's action method to an empty -unwindToRed: method without overriding -segueForUnwindingToViewController: (e.g. CTRL+drag from the button in the blue VC to the blue VC's "Exit" glyph in IB and select -unwindToRed: from the list.)

  3. If #2 does not dismiss the blue VC, you could try invoking the following manually in -unwindToRed: IBAction method:

    [self dismissViewControllerAnimated:NO completion:nil];