8
votes

I have a view controller presenting another view controller with modalPresentationStyle = UIModalPresentationCustom. Things are set up so that part of the presenting view controller's view shows up underneath the presented view controller's view. In this state, the presenting view controller still handles auto-rotation correctly, and I handle rotation for the presented view controller using autolayout.

I'm now trying to implement interactively dismissing the presented view controller using iOS 7's custom view controller transitioning API. It works except that, when the interactive dismissal is canceled, handling of auto-rotation stops working. (It works again after the presented view controller is dismissed later.) Why is this happening, and how can I fix this?

EDIT: Here is code you can run to demonstrate the problem. A view pops up from below, and you can dismiss it by swiping it down. If you cancel dismissal by not swiping it all the way down, the presenting view controller's view no longer responds to rotations, and the presented view controller's view has messed-up layout.

EDIT: Here is the link to the code below as an Xcode project: https://drive.google.com/file/d/0BwcBqUuDfCG2YlhVWE1QekhUWlk/edit?usp=sharing

Sorry for the massive code dump, but I don't know what I'm doing wrong. Here's a sketch of what is going on: ViewController1 presents ViewController2. ViewController1 implements UIViewControllerTransitioningDelegate, so it is returning the animation/interactive controllers for the transitions. ViewController2 has a pan gesture recognizer that drives the interactive dismissal; it implements UIViewControllerInteractiveTransitioning to serve as the interactive controller for dismissal. It also keeps a reference to the animation controller for dismissal to finish the transition if the user drags the view down far enough. Finally, there are two animation controller objects. PresentAnimationController sets up the autolayout constraints to handle rotations for the presented view controller's view, and DismissAnimationController finishes up the dismissal.

ViewController1.h

#import <UIKit/UIKit.h>

@interface ViewController1 : UIViewController <UIViewControllerTransitioningDelegate>

@end

ViewController1.m

#import "ViewController1.h"

#import "ViewController2.h"

#import "PresentAnimationController.h"
#import "DismissAnimationController.h"

@implementation ViewController1

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        self.title = @"View 1";

        self.navigationItem.prompt = @"Press “Present” and then swipe down to dismiss.";
        self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Present" style:UIBarButtonItemStylePlain target:self action:@selector(pressedPresentButton:)];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];

    // Some subview just to check if layout is working.
    UIView * someSubview = [[UIView alloc] initWithFrame:self.view.bounds];
    someSubview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    someSubview.backgroundColor = [UIColor orangeColor];
    someSubview.layer.borderColor = [UIColor redColor].CGColor;
    someSubview.layer.borderWidth = 2;
    [self.view addSubview:someSubview];
}

// --------------------

- (void)pressedPresentButton:(id)sender
{
    ViewController2 * presentedVC = [[ViewController2 alloc] initWithNibName:nil bundle:nil];

    presentedVC.modalPresentationStyle = UIModalPresentationCustom;
    presentedVC.transitioningDelegate = self;

    [self presentViewController:presentedVC animated:YES completion:nil];
}

// --------------------

// View Controller Transitioning Delegate Methods.

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [[PresentAnimationController alloc] init];;
}

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    DismissAnimationController * animationController = [[DismissAnimationController alloc] init];

    ViewController2 * presentedVC = (ViewController2 *)self.presentedViewController;

    if (presentedVC.dismissalIsInteractive) {
        presentedVC.dismissAnimationController = animationController;
    }

    return animationController;
}

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator
{
    return nil;
}

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator
{
    ViewController2 * presentedVC = (ViewController2 *)self.presentedViewController;

    if (presentedVC.dismissalIsInteractive) {
        return presentedVC;
    }
    else {
        return nil;
    }
}

@end

ViewController2.h

#import <UIKit/UIKit.h>

#import "DismissAnimationController.h"

@interface ViewController2 : UIViewController <UIViewControllerInteractiveTransitioning>

@property (weak, nonatomic) UIView * contentView;

@property (nonatomic, readonly) BOOL dismissalIsInteractive;
@property (strong, nonatomic) DismissAnimationController * dismissAnimationController;

@end

ViewController2.m

#import "ViewController2.h"

@interface ViewController2 ()

@property (strong, nonatomic) id<UIViewControllerContextTransitioning> transitionContext;

@end

@implementation ViewController2

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        _dismissalIsInteractive = NO;
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];

    // Set up content view.
    CGRect frame = UIEdgeInsetsInsetRect(self.view.bounds, UIEdgeInsetsMake(15, 15, 15, 15));
    UIView * contentView = [[UIView alloc] initWithFrame:frame];
    self.contentView = contentView;
    contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    contentView.backgroundColor = [UIColor cyanColor];
    contentView.layer.borderColor = [UIColor blueColor].CGColor;
    contentView.layer.borderWidth = 2;
    [self.view addSubview:contentView];

    // Set up pan dismissal gesture recognizer.
    UIPanGestureRecognizer * panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dismissalPan:)];
    [self.view addGestureRecognizer:panGesture];
}

// --------------------

- (void)dismissalPan:(UIPanGestureRecognizer *)panGesture
{
    switch (panGesture.state) {
        case UIGestureRecognizerStateBegan: {
            _dismissalIsInteractive = YES;

            [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];

            break;
        }

        case UIGestureRecognizerStateChanged: {
            CGPoint translation = [panGesture translationInView:self.view];

            CGFloat percent;
            if (translation.y > 0) {
                percent = translation.y / self.view.bounds.size.height;
                percent = MIN(percent, 1.0);
            }
            else {
                percent = 0;
            }

            // Swiping content view down.
            CGPoint center;
            center.x = CGRectGetMidX(self.view.bounds);
            center.y = CGRectGetMidY(self.view.bounds);
            if (translation.y > 0) {
                center.y += translation.y;  // Only allow swiping down.
            }
            self.contentView.center = center;

            self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:(0.5 * (1.0 - percent))];

            [self.transitionContext updateInteractiveTransition:percent];

            break;
        }

        case UIGestureRecognizerStateEnded: // Fall through.
        case UIGestureRecognizerStateCancelled: {
            _dismissalIsInteractive = NO;

            id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
            self.transitionContext = nil;

            DismissAnimationController * dismissAnimationController = self.dismissAnimationController;
            self.dismissAnimationController = nil;

            CGPoint translation = [panGesture translationInView:self.view];

            if (translation.y > 100) {
                // Complete dismissal.

                [dismissAnimationController animateTransition:transitionContext];
            }
            else {
                // Cancel dismissal.

                void (^animations)() = ^() {
                    CGPoint center;
                    center.x = CGRectGetMidX(self.view.bounds);
                    center.y = CGRectGetMidY(self.view.bounds);
                    self.contentView.center = center;

                    self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
                };
                void (^completion)(BOOL) = ^(BOOL finished) {
                    [transitionContext cancelInteractiveTransition];
                    [transitionContext completeTransition:NO];
                };
                [UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:animations completion:completion];
            }

            break;
        }

        default: {

            break;
        }
    }
}

// --------------------

// View Controller Interactive Transitioning Methods.

- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    self.transitionContext = transitionContext;
}

@end

PresentAnimationController.h

#import <Foundation/Foundation.h>

@interface PresentAnimationController : NSObject <UIViewControllerAnimatedTransitioning>

@end

PresentAnimationController.m

#import "PresentAnimationController.h"

#import "ViewController2.h"

@implementation PresentAnimationController

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

    UIView * containerView = [transitionContext containerView];

    CGPoint toCenter = fromVC.view.center;
    CGRect toBounds = fromVC.view.bounds;

    toVC.view.center = toCenter;
    toVC.view.bounds = toBounds;
    [toVC.view layoutIfNeeded];

    [containerView addSubview:fromVC.view];
    [containerView addSubview:toVC.view];

    CGRect contentViewEndFrame = toVC.contentView.frame;

    CGRect contentViewStartFrame = contentViewEndFrame;
    contentViewStartFrame.origin.y += contentViewStartFrame.size.height;
    toVC.contentView.frame = contentViewStartFrame;

    UIColor * endBackgroundColor = toVC.view.backgroundColor;

    toVC.view.backgroundColor = [UIColor clearColor];

    void (^animations)() = ^() {
        toVC.contentView.frame = contentViewEndFrame;

        toVC.view.backgroundColor = endBackgroundColor;
    };
    void (^completion)(BOOL) = ^(BOOL finished) {
        toVC.view.autoresizingMask = UIViewAutoresizingNone;

        toVC.view.translatesAutoresizingMaskIntoConstraints = NO;

        NSLayoutConstraint * centerXConstraint = [NSLayoutConstraint constraintWithItem:toVC.view
                                                                              attribute:NSLayoutAttributeCenterX
                                                                              relatedBy:NSLayoutRelationEqual
                                                                                 toItem:fromVC.view
                                                                              attribute:NSLayoutAttributeCenterX
                                                                             multiplier:1
                                                                               constant:0];
        NSLayoutConstraint * centerYConstraint = [NSLayoutConstraint constraintWithItem:toVC.view
                                                                              attribute:NSLayoutAttributeCenterY
                                                                              relatedBy:NSLayoutRelationEqual
                                                                                 toItem:fromVC.view
                                                                              attribute:NSLayoutAttributeCenterY
                                                                             multiplier:1
                                                                               constant:0];
        NSLayoutConstraint * widthConstraint = [NSLayoutConstraint constraintWithItem:toVC.view
                                                                            attribute:NSLayoutAttributeWidth
                                                                            relatedBy:NSLayoutRelationEqual
                                                                               toItem:fromVC.view
                                                                            attribute:NSLayoutAttributeWidth
                                                                           multiplier:1
                                                                             constant:0];
        NSLayoutConstraint * heightConstraint = [NSLayoutConstraint constraintWithItem:toVC.view
                                                                             attribute:NSLayoutAttributeHeight
                                                                             relatedBy:NSLayoutRelationEqual
                                                                                toItem:fromVC.view
                                                                             attribute:NSLayoutAttributeHeight
                                                                            multiplier:1
                                                                              constant:0];
        [containerView addConstraint:centerXConstraint];
        [containerView addConstraint:centerYConstraint];
        [containerView addConstraint:widthConstraint];
        [containerView addConstraint:heightConstraint];

        [transitionContext completeTransition:YES];
    };
    [UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:animations completion:completion];
}

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.5;
}

@end

DismissAnimationController.h

#import <Foundation/Foundation.h>

@interface DismissAnimationController : NSObject <UIViewControllerAnimatedTransitioning>

@end

DismissAnimationController.m

#import "DismissAnimationController.h"

#import "ViewController2.h"

@implementation DismissAnimationController

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

    UIView * containerView = [transitionContext containerView];

    [containerView addSubview:toVC.view];
    [containerView addSubview:fromVC.view];

    void (^animations)() = ^() {
        CGRect contentViewEndFrame = fromVC.contentView.frame;
        contentViewEndFrame.origin.y = CGRectGetMaxY(fromVC.view.bounds) + 15;
        fromVC.contentView.frame = contentViewEndFrame;

        fromVC.view.backgroundColor = [UIColor clearColor];
    };
    void (^completion)(BOOL) = ^(BOOL finished) {
        if ([transitionContext isInteractive]) {
            [transitionContext finishInteractiveTransition];
        }

        [transitionContext completeTransition:YES];
    };
    [UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveLinear animations:animations completion:completion];
}

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.5;
}

@end

AppDelegate.m

#import "AppDelegate.h"

#import "ViewController1.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    ViewController1 * vc = [[ViewController1 alloc] initWithNibName:nil bundle:nil];
    UINavigationController * nav = [[UINavigationController alloc] initWithRootViewController:vc];
    self.window.rootViewController = nav;

    [self.window makeKeyAndVisible];
    return YES;
}

@end
1
A big bounty! Good luck, hope you get through soon.John
So it drops out entirely? No calls to shouldAutorotate or any of the other rotation methods?mattsven
@mattsven It appears so. I once did an NSLog on willRotateToInterfaceOrientation:... for ViewController1, and nothing happened. (I didn't try shouldAutorotate.)ememem
@mattsven Actually, it seems that ViewController2 is getting the rotation notifications, but its layout is still messed up.ememem
@user2135004 So the bounds/frame are jacked up?mattsven

1 Answers

3
votes

I think I found your problem. in your PresentAnimationController.m you specify toVC.view.translatesAutoresizingMaskIntoConstraints = NO; and you set all of your constraints in the completion block you set in - (void)animateTransition:

Comment that line out and all of the constraints and addConstraint: calls and it should work

EDIT:

Just saw it worked only when the gesture was cancelled and not when the view is initially displayed. Comment out everything in the completion block except for

[transitionContext completeTransition:YES];