Michael Hsu
Michael Hsu

Reputation: 982

swift 4 - how do I run code in the destination view controller AFTER the segue animation is finished?

So I have 2 view controllers, and I want to get from view controller 1 to view controller 2 with a custom animation. Here is the code for my custom animation:

let transition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromRight
self.view.window?.layer.add(transition, forKey: nil)

I run this before I call performSegue(), and it works. But what I want to do is in the code for view controller 2, I want to run something after the segue animation finishes (so after the 0.5 seconds). My view controllers are not part of a navigation controller, so this post doesn't help. I also want my code to be in the target view controller, but this post has it in the source view controller, so that doesn't help either.

I've tried testing viewDidLoad() and viewDidAppear(), but they both run before the sliding animation is finished. Please help, thanks!

Upvotes: 2

Views: 596

Answers (1)

Rob
Rob

Reputation: 438467

When you animate your transition correctly, viewDidAppear will be called when the animation is done. See Customizing Transition Animations in View Controller Programming Guide for iOS for instructions on the proper way to customize a transition between two view controllers.

As that guide says, when you want to customize a modal transition, you should specify a modalPresentationStyle of .custom and then specify a transitioningDelegate which will supply:

  • Presentation controller;
  • Animation controller for presenting a modal; and
  • Animation controller for dismissing a modal

For example, the destination view controller would specify that it will do a custom transition:

class ViewController: UIViewController {

    // if storyboards, override `init(coder:)`; if NIBs or programmatically 
    // created view controllers, override the appropriate `init` method.

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        transitioningDelegate = self
        modalPresentationStyle = .custom
    }

    ...

}

And, in its UIViewControllerTransitioningDelegate, it vends that presentation controller and the animation controllers:

extension ViewController: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TransitionAnimator(operation: .present)
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TransitionAnimator(operation: .dismiss)
    }

    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return PresentationController(presentedViewController: presented, presenting: presenting)
    }
}

All the presentation controller does is to specify that the presenter's view should be removed from the view hierarchy when the transition is done (which is the rule of thumb unless the presenting view is translucent or doesn't cover the whole screen):

class PresentationController: UIPresentationController {
    override var shouldRemovePresentersView: Bool { return true }
}

And the animator specifies the duration and the particular details of the animation:

class TransitionAnimator: NSObject {

    enum TransitionOperation {
        case present
        case dismiss
    }

    private let operation: TransitionOperation

    init(operation: TransitionOperation) {
        self.operation = operation
        super.init()
    }
}

extension TransitionAnimator: UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.0
    }

    func animateTransition(using context: UIViewControllerContextTransitioning) {
        let toVC = context.viewController(forKey: .to)!
        let fromVC = context.viewController(forKey: .from)!
        let container = context.containerView

        let frame = fromVC.view.frame
        let rightFrame = CGRect(origin: CGPoint(x: frame.origin.x + frame.width, y: frame.origin.y), size: frame.size)
        let leftFrame = CGRect(origin: CGPoint(x: frame.origin.x - frame.width, y: frame.origin.y), size: frame.size)

        switch operation {
        case .present:
            toVC.view.frame = rightFrame
            container.addSubview(toVC.view)
            UIView.animate(withDuration: transitionDuration(using: context), animations: {
                toVC.view.frame = frame
                fromVC.view.frame = leftFrame
            }, completion: { finished in
                fromVC.view.frame = frame
                context.completeTransition(!context.transitionWasCancelled)
            })

        case .dismiss:
            toVC.view.frame = leftFrame
            container.addSubview(toVC.view)
            UIView.animate(withDuration: transitionDuration(using: context), animations: {
                toVC.view.frame = frame
                fromVC.view.frame = rightFrame
            }, completion: { finished in
                fromVC.view.frame = frame
                context.completeTransition(!context.transitionWasCancelled)
            })
        }
    }
}

Obviously, do whatever animation you want, but hopefully you get the basic idea. Bottom line, the destination view controller should specify its transitioningDelegate and then you can just do a standard modal presentation (either via present or show or just a segue), and your transition animation will be customized and your destination's viewDidAppear will be called when the animation is done.

Upvotes: 5

Related Questions