40
votes

So I built a custom presenting transition animation and everything seems to be working great except the view controller lifecycle methods are not being called on dismiss.

Before presenting the controller I use the UIModalPresentationCustom style to keep the presenting VC in the view hierarchy, but once I dismiss the presented VC, viewWillAppear and viewDidAppear are not called on my presenting controller. Am I missing a step that I need to explicitly call to get those methods to fire? I know manually calling those methods is not the correct solution.

Here is my dismissing animation code. I'm basically animating a form overlay view to shrink to the size of a collection view cell on dismissal.

- (void)_animateDismissingTransitionWithContext:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    UICollectionView *destinationCollectionView = toCollectionViewController.collectionView;
    UICollectionViewCell *destinationCollectionViewCell = [self _destinationCellFromContext:transitionContext];
    UIView *containerView = transitionContext.containerView;

    // Calculate frames    
    CGRect startFrame = fromEventDetailViewController.detailContainerView.frame;
    CGRect endFrame = [destinationCollectionView convertRect:destinationCollectionViewCell.frame toView:containerView];

    // Add overlay
    UIView *overlayView = [UIView new];
    overlayView.backgroundColor = [UIColor overlayBackground];
    overlayView.frame = containerView.bounds;
    overlayView.alpha = 1.0f;
    [containerView addSubview:overlayView];

    // Add fake detail container view
    UIView *fakeContainerView = [UIView new];
    fakeContainerView.backgroundColor = fromEventDetailViewController.detailContainerView.backgroundColor;
    fakeContainerView.frame = startFrame;
    [containerView addSubview:fakeContainerView];

    // Hide from view controller
    fromEventDetailViewController.view.alpha = 0.0f;

    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:0.2f options:UIViewAnimationOptionCurveEaseOut animations:^{
        fakeContainerView.frame = endFrame;
        fakeContainerView.backgroundColor = [UIColor eventCellBackground];

        overlayView.alpha = 0.0f;
    } completion:^(BOOL finished) {
        [fromEventDetailViewController.view removeFromSuperview];
        [overlayView removeFromSuperview];
        [fakeContainerView removeFromSuperview];

        [transitionContext completeTransition:YES];
    }];
}
6
Can you post your UIViewControllerAnimatedTransitioning code?d2burke
Posted the dismissal code. Let me know if any other info would help.Danny

6 Answers

43
votes

Another solution could be using beginAppearanceTransition: and endAppearanceTransition:. According to documentation:

If you are implementing a custom container controller, use this method to tell the child that its views are about to appear or disappear. Do not invoke viewWillAppear:, viewWillDisappear:, viewDidAppear:, or viewDidDisappear: directly.

Here is how I used them:

- (void)animationEnded:(BOOL)transitionCompleted
{
    if (!transitionCompleted)
    {
        _toViewController.view.transform = CGAffineTransformIdentity;
    }
    else
    {
        [_toViewController endAppearanceTransition];
    }
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    [toViewController beginAppearanceTransition:YES animated:YES];
    // ... other code
}

But I still consider strange that custom modal presentation not doing this.

14
votes

If you are using UIModalPresentationCustom you should provide custom UIPresentationController class, and if you want to use ViewController lifecycle callers, you need to override shouldRemovePresentersView and return YES.

If you would like to keep presenters and still have ViewControlelr lifecycle callback, you can override private method _shouldDisablePresentersAppearanceCallbacks and return NO in your custom UIPresentationController class.

10
votes

After much wrangling with this issue, I found the best solution which works in ios7 and ios8 is to leave modalPresentationStyle = UIModalPresentationFullScreen instead of UIModalPresentationCustom as the docs suggest.

If i do this as well as setting the transitioningDelegate to my delegate, it still respects my transition and the will/diddisappear methods get called on the 'from' view controller. Also: no present-then-rotate-then-dismiss rotation issues to boot.

4
votes

@John Tracids' anser solved my issue. Thanks John!

But I would like to extend an answer a bit.

If you are presenting UIViewController instance with modalPresentationStyle = .custom (objc UIModalPresentationCustom) in order to keep viewcontroller's lifecycle methods being called, you have to manage viewcontroller’s appearance explicitly. To do that just call beginAppearanceTransition before animation and endAppearanceTransition at the animation completion block.

Also you can pass to your transitioning animator class custom UIPresentationController subclass with overridden value shouldRemovePresentersView returning true without calling beginAppearanceTransition

// Swift 4

put this to your custom UIViewControllerAnimatedTransitioning class before animation

fromViewController.beginAppearanceTransition(false, animated: true)
toViewController.beginAppearanceTransition(true, animated: true)

UIView.animate(withDuration: animationDuration, animations: {
        // animation logic…
    }) { finished in
        fromViewController.endAppearanceTransition()
        toViewController.endAppearanceTransition()
        let transitionSuccess = !transitionContext.transitionWasCancelled
        transitionContext.completeTransition(transitionSuccess)
    }

// UIPresentationController subclass
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}
3
votes

Ah, this is a modal presentation. I don't believe viewWillAppear and viewDidAppear are called with custom transition using the method, as the view is technically still active in the view hierarchy. I'd suggest using delegation here as you normally would with a presented modal.

Create delegate protocol on the presented VC. Create a delegate method that can be called from the presenting VC. As you present the overlay, set the presenting VC as the delegate. Then, call that delegate method from the presented VC. Then you can call any sort of actions from within the presenting VC before you call dismissViewController

In your overlay (ModalViewController.h):

@protocol ModalViewDelegate <NSObject>
-(void)didDismissModalView;
@end

@interface ModalViewController : UIViewController
@property(strong, nonatomic) id <ModalViewDelegate> delegate;

In your ModalViewController.m, call a method that calls your delegate method:

- (void)dismissModal{
    [self.delegate didDismissModalView];
}

In your presenting VC h file: (PresentingViewController.h), make this class conform to your modal delegate protocol:

@interface PresentingViewController : UIViewController <ModalViewDelegate>

In your presenting VC, as you present the modal:

...
ModalViewController *modalViewController = [[ModalViewController alloc] init];
modalViewController.delegate = self; //the makes the presenting VC the delegate
[self presentViewController:modalViewController animated:YES completion:nil];
...

Finally, in your presenting VC, when you want to perform some actions before dismissing the modal, implement the ModalViewDelegate method:

- (void)didDismissModalView{
    //DO SOME COOL STUFF, SET UP STUFF HERE, UPDATE UI, ETC

    //Then dismiss the modal
   [self dismissViewControllerAnimated:YES completion:^{
      //Do more cool stuff
    }];
}

All of this will work with your current custom transition code, but will give you more control over what happens before the modal/overlay is dismissed. Delegate is a beautiful thing.

Further, this will keep this animation code separate from code for other functionality, which is a bit cleaner IMO.

0
votes

I have the same problem to call viewWillAppear and other lifecycle methods. What I did to solve it was implemented the delegate method func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?

Then to make it work I do the following:

class ViewController: UIViewController {
... 

func showViewController() {
// load your view controller as you want
    guard let detailViewController = loadDetailViewcontroller()  as? DetailViewController else {
            return }
        detailViewController.modalPresentationStyle = .custom
        detailViewController.transitioningDelegate = self
        present(detailViewController, animated: true, completion: nil)
  }
}

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

The PresentationController is like a temporary object for the presentation. Apple's documentation

From the time a view controller is presented until the time it is dismissed, UIKit uses a presentation controller to manage various aspects of the presentation process for that view controller. The presentation controller can add its own animations on top of those provided by animator objects, it can respond to size changes, and it can manage other aspects of how the view controller is presented onscreen.