IOS 7+ Dismiss Modal View Controller и Force Portrait Orientation

У меня есть UINavigationController в качестве контроллера корневого представления моего UIWindow на iOS 7 и iOS 8. С одного из своих контроллеров представления я представляю полноэкранный modal view controller с перекрещивающимся стилем презентации. Этот контроллер модального представления должен иметь возможность вращаться ко всем ориентациям, и он отлично работает.

Проблема заключается в том, что устройство удерживается в альбомной ориентации, а диспетчер модального просмотра отклоняется. Контроллер представления, который представил модальный, поддерживает только портретную ориентацию, и я подтвердил, что UIInterfaceOrientationMaskPortrait возвращается в -application: supportedInterfaceOrientationsForWindow:. -shouldAutorotate возвращает ДА, также. Однако ориентация контроллера представления зрения после отклонения модальности остается ландшафтом. Как я могу заставить его оставаться в портретной ориентации, позволяя модальным воспринимать ориентацию устройства? Мой код:

Делегат приложения:

- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
        UINavigationController *navigationController = (UINavigationController *)self.deckController.centerController;
        NSArray *viewControllers = [navigationController viewControllers];
        UIViewController *top = [viewControllers lastObject];

        if (top && [top presentedViewController]) {
            UIViewController *presented = [top presentedViewController];
            if ([presented respondsToSelector:@selector(isDismissing)] && ![(id)presented isDismissing]) {
                top = presented;
            }
        }

        return [top supportedInterfaceOrientations];
    }

    return (UIInterfaceOrientationMaskLandscapeLeft|UIInterfaceOrientationMaskLandscapeRight);
}

Представление контроллера представления:

- (BOOL)shouldAutorotate {
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationPortrait;
}

Модальный контроллер:

- (BOOL)shouldAutorotate
{
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations
{
    return (UIInterfaceOrientationMaskLandscape|UIInterfaceOrientationMaskLandscapeLeft|UIInterfaceOrientationMaskPortrait);
}

Ответ 1

Если модальный контроллер находился в альбомной ориентации перед увольнением, отображающий ViewController может не вернуться к исходной ориентации (портрет). Проблема заключается в том, что метод AppDelegate supportedInterfaceOrientationsForWindow вызывается до того, как контроллер фактически уволен, и представленная проверка контроллера все еще возвращает маску ландшафта.

Установите флаг, указывающий, будет ли отображаться (модальный) представленный контроллер просмотра.

- (void)awakeFromNib // or where you instantiate your ViewController from
{
    [super awakeFromNib];
    self.presented = YES;
}

- (IBAction)exitAction:(id)sender // where you dismiss the modal
{
    self.presented = NO;
    [self dismissViewControllerAnimated:NO completion:nil];
}

И в представленном модальном представлении ViewController задайте ориентацию в соответствии с флагом: Когда представлен модальный ViewController - возвратите Пейзаж. Когда он уволен, верните портрет

- (NSUInteger)supportedInterfaceOrientations
{
    if ([self isPresented]) {
        return UIInterfaceOrientationMaskLandscape;
    } else {
        return UIInterfaceOrientationMaskPortrait;
    }
}

Последний шаг - из вашего AppDelegate вызывается модальный представленный ViewController для его ориентации. Я просто проверяю представленный в настоящее время ViewController и вызываю на нем поддержкуInterfaceOrientations

- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
    NSUInteger orientationMask = UIInterfaceOrientationMaskPortrait;

    UIViewController *currentVC = self.window.rootViewController.presentedViewController; // gets the presented VC
    orientationMask = [currentVC supportedInterfaceOrientations];

    return orientationMask;
}

Подробнее... эта ссылка

Ответ 2

В итоге я подклассифицировал UINavigationController и переопределив его методы вращения. Следующее решение работает на iOS 7, но я считаю, что в iOS 8 beta 5 есть ошибка, из-за которой представление контроллера представления уменьшится до половины высоты экрана после отклонения модальности в альбомной ориентации.

Подкласс UINavigationController:

- (BOOL)shouldAutorotate
{
    return NO;
}

- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
    return UIInterfaceOrientationPortrait;
}

Ответ 3

Это решение для iOS 8 +.


Описание проблемы

  • Ключ приложения имеет подкласс UINavigationController в качестве его rootViewController.
  • Этот подкласс NC запрещает некоторые ориентации интерфейса.
  • Некоторые контроллеры представлений (VC1) в стеке ЧПУ представляют другой вид контроллера (VC2) по умолчанию и в полноэкранном режиме.
  • Представленный VC2 позволяет больше ориентации интерфейса, чем NC.
  • Пользователь поворачивает устройство по ориентации, которое запрещено NC, но разрешено представленным VC2.
  • Пользователь отклоняет представленный VC2.
  • Вид VC1 имеет неправильный фрейм.

Настройка и иллюстрация

Подкласс UINavigationController:

- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}

- (BOOL)shouldAutorotate
{
    return YES;
}

Начальный внешний вид VC1 и стек представления пользовательского интерфейса:

Initial appearance

Представление VC2 (QLPreviewController в этом примере) из VC1:

QLPreviewController *pc = [[QLPreviewController alloc] init];
pc.dataSource = self;
pc.delegate = self;
pc.modalPresentationStyle = UIModalPresentationFullScreen;
[self.navigationController presentViewController:pc animated:YES completion:nil];

Представлен VC2, а устройство повернуто к ландшафту:

Presented and rotated

VC2 уволен, устройство вернулось в портретном режиме, но стек NC остается в ландшафте:

VC2 dismissed


Причина

Документация Apple гласит:

Когда вы представляете контроллер вида с помощью метода presentViewController: анимированный: завершение: UIKit всегда управляет процессом презентации. Часть этого процесса включает создание контроллера представления, который подходит для данного стиля презентации.

По-видимому, есть ошибка при обработке стека UINavigationController.


Решение

Эта ошибка может быть обойдена путем предоставления нашего собственного делегата перехода.

BTTransitioningDelegate.h

#import <UIKit/UIKit.h>

@interface BTTransitioningDelegate : NSObject <UIViewControllerTransitioningDelegate>

@end

BTTransitioningDelegate.m

#import "BTTransitioningDelegate.h"

static NSTimeInterval kDuration = 0.5;

// This class handles presentation phase.
@interface BTPresentedAC : NSObject <UIViewControllerAnimatedTransitioning>

@end

@implementation BTPresentedAC

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

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)context
{
    // presented VC
    UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];

    // presented controller ought to be fullscreen
    CGRect frame = [[[UIApplication sharedApplication] keyWindow] bounds];
    // we will slide view of the presended VC from the bottom of the screen,
    // so here we set the initial frame
    toVC.view.frame = CGRectMake(frame.origin.x, frame.origin.y + frame.size.height, frame.size.width, frame.size.height);

    // [context containerView] acts as the superview for the views involved in the transition
    [[context containerView] addSubview:toVC.view];

    UIViewAnimationOptions options = (UIViewAnimationOptionCurveEaseOut);

    [UIView animateWithDuration:kDuration delay:0 options:options animations:^{
        // slide view to position
        toVC.view.frame = frame;
    } completion:^(BOOL finished) {
        // required to notify the system that the transition animation is done
        [context completeTransition:finished];
    }];
}

@end


// This class handles dismission phase.
@interface BTDismissedAC : NSObject <UIViewControllerAnimatedTransitioning>

@end

@implementation BTDismissedAC

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

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)context
{
    // presented VC
    UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
    // presenting VC
    UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey];

    // inserting presenting VC view under presented VC view
    toVC.view.frame = [[[UIApplication sharedApplication] keyWindow] bounds];
    [[context containerView] insertSubview:toVC.view belowSubview:fromVC.view];

    // current frame and transform of presented VC
    CGRect frame = fromVC.view.frame;
    CGAffineTransform transform = fromVC.view.transform;

    // determine current presented VC view rotation and assemble
    // target frame to provide naturally-looking dismissal animation
    if (transform.b == -1) {
        // -pi/2
        frame = CGRectMake(frame.origin.x + frame.size.width, frame.origin.y, frame.size.width, frame.size.height);
    } else if (transform.b == 1) {
        // pi/2
        frame = CGRectMake(frame.origin.x - frame.size.width, frame.origin.y, frame.size.width, frame.size.height);
    } else if (transform.a == -1) {
        // pi
        frame = CGRectMake(frame.origin.x, frame.origin.y - frame.size.height, frame.size.width, frame.size.height);
    } else {
        // 0
        frame = CGRectMake(frame.origin.x, frame.origin.y + frame.size.height, frame.size.width, frame.size.height);
    }

    UIViewAnimationOptions options = (UIViewAnimationOptionCurveEaseOut);

    [UIView animateWithDuration:kDuration delay:0 options:options animations:^{
        // slide view off-screen
        fromVC.view.frame = frame;
    } completion:^(BOOL finished) {
        // required to notify the system that the transition animation is done
        [context completeTransition:finished];
    }];
}

@end


@implementation BTTransitioningDelegate

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

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [[BTDismissedAC alloc] init];
}

@end

Импортируйте делегат, который переводит делегат при представлении VC:

#import "BTTransitioningDelegate.h"

Храните сильную ссылку на экземпляр:

@property (nonatomic, strong) BTTransitioningDelegate *transitioningDelegate;

Произведите активацию в -viewDidLoad:

self.transitioningDelegate = [[BTTransitioningDelegate alloc] init];

Вызовите при необходимости:

QLPreviewController *pc = [[QLPreviewController alloc] init];
pc.dataSource = self;
pc.delegate = self;
pc.transitioningDelegate = self.transitioningDelegate;
pc.modalPresentationStyle = UIModalPresentationFullScreen;

[self.navigationController presentViewController:pc animated:YES completion:nil];