View controller does not auto-rotate after canceling interactive dismissal transition for presented view controller
Asked Answered
J

1

8

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
Janejanean answered 8/8, 2014 at 22:14 Comment(9)
A big bounty! Good luck, hope you get through soon.Gamali
So it drops out entirely? No calls to shouldAutorotate or any of the other rotation methods?Prole
@Prole It appears so. I once did an NSLog on willRotateToInterfaceOrientation:... for ViewController1, and nothing happened. (I didn't try shouldAutorotate.)Janejanean
@Prole Actually, it seems that ViewController2 is getting the rotation notifications, but its layout is still messed up.Janejanean
@user2135004 So the bounds/frame are jacked up?Prole
@Prole The view for ViewController2 still thinks it has the bounds it previously had (e.g., it maintains portrait bounds in landscape orientation). Its frame seems to be aligned to some side of the screen as opposed to being in the center. Based on my autolayout constraints, it's supposed to shadow the layout of the view for ViewController1. The view for ViewController1 stays in the orientation it previously had.Janejanean
@user2135004 This might be weird, but try setting self.view.bounds in viewDidLayoutSubviews to (CGRect){0, 0, screenWidth, screenHeight} manuallyProle
Something interesting that I noted (and didn't find in your question) is that even though the screen gets messed up, when you drag the view down, it kind of "recovers" itself and everything looks normal again.Watts
I came up with a work-around for this problem: I just didn't use iOS 7's custom view controller transition API for the interactive part. Instead, I can just do the interactive part myself in ViewController2.Janejanean
D
3

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];

Dewyeyed answered 15/8, 2014 at 18:15 Comment(3)
Also note that the VC underneath the modal will NOT autorotate. This is because when a modal is present, no other views receive auto rotate notifications. Refer to this answerDewyeyed
Your solution "fixes" the layout of view controller 2, but the reason I used autolayout instead of autoresizing mask is to handle the case when the in-call status bar shows and hides. With autoresizing mask, when the in-call status bar disappears, the top part of view controller 2 is no longer aligned to the top but is one status bar below the top. Also, part of the reason why view controller 2 layout is messed up with autolayout is because of the "equal width/height" constraints in relation to view controller 1, which is stuck in its old interface orientation after rotation.Janejanean
The answer you referred me to about a presenting view controller not getting rotation notifications is talking about the API in iOS 5 when presented view controllers on the iPhone were only full screen, so the presenting view controller is completely covered up. In that case, the presenting view controller will get viewWill/DidDissappear, and I no longer have to worry about it anymore. However, in the case of my question, part of the presenting view controller is still showing underneath, and it responds to rotations up until canceling the interactive transition.Janejanean

© 2022 - 2024 — McMap. All rights reserved.