Разбираемся с autorotation в iOS 6

Привет, друзья!

Если вы создаете приложения для гаджетов от Apple, то наверняка в курсе, что недавно произошло обновление iOS до версии 6.
Наравне с другими новыми функциями Apple внесла изменения в механизм autorotation.
На всякий случай напомню, что autorotation — это механизм, позволяющий использовать устройство как в портретной (вытянутой в высоту), так в альбомной (растянутой в ширину) ориентации, а также изменять эту ориентацию при повороте устройства.
image
Если в вашем приложении контент отображается в обеих ориентациях (а особенно если на некоторых экранах вам нужно запретить поворот) — готов поспорить, что у вас уже возникли некоторые вопросы.
Если же вы не используете функцию изменения ориентации экрана — разницы могли и не заметить. Однако знание того, как в iOS6 работает autorotation, в любом случае будет полезно и пригодится в будущем.

Как было до iOS 6


Устройства под управлением iOS поддерживают 4 возможных ориентации экрана, описываемых соответствующими системными константами:
  • UIInterfaceOrientationPortrait
  • UIInterfaceOrientationPortraitUpsideDown
  • UIInterfaceOrientationLandscapeLeft
  • UIInterfaceOrientationLandscapeRight

В iOS 5 и более ранних версиях, для работы механизма autorotation используется метод

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
   // Возвращаем YES для поддерживаемых ориентаций
   return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

Когда устройство изменяет свою ориентацию в пространстве, при помощи вызова данного метода система запрашивает активный контроллер (view controller), поддерживает ли он переход в эту ориентацию.
При вызове метода параметр interfaceOrientation содержит одно из 4 возможных значений, и метод должен вернуть YES, если требуется повернуть окно приложения (или NO в противном случае).
Таким образом, для каждого отдельного контроллера достаточно переопределить метод shouldAutorotateToInterfaceOrientation: и указать поддерживаемые им виды ориентации.

Ключ UISupportedInterfaceOrientations в Info.plist содержит список ориентаций, поддерживаемых приложением (также можно выбрать их в разделе Summary ваших Targets) и используется системой только для определения начальной ориентации при запуске приложения.



Если не будет указано ни одного вида ориентации — ничего страшного не произойдет, приложение будет запущено в обычной портретной (UIInterfaceOrientationPortrait).

Стало в iOS 6


В iOS 6 метод shouldAutorotateToInterfaceOrientation объявлен устаревшим (deprecated), а за логику работы autorotation отвечают два других — supportedInterfaceOrientations и shouldAutorotate.

При изменения положения устройства (или когда контроллер презентуется модально) система опрашивает самый верхний полноэкранный контроллер (top-most full-screen view controller). При этом сначала происходит вызов shouldAutorotate, а затем (только в случае возврата значения YES) — вызов supportedInterfaceOrientations для получения битовой маски, описывающей поддерживаемые положения. Например, следущий код может использоваться для подержки обычной портретной и обеих альбомных ориентаций.

- (NSInteger)supportedInterfaceOrientations
{
   return UIInterfaceOrientationMaskPortrait | 
          UIInterfaceOrientationMaskLandscapeLeft | 
          UIInterfaceOrientationMaskLandscapeRight;
}

Далее, система использует полученное от supportedInterfaceOrientations значение, производя операцию конъюнкции (побитовое «И») со списком глобально поддерживаемых приложением ориентаций (берется из Info.plist либо как результат метода AppDelegate application:supportedInterfaceOrientationsForWindow:). По итогам проведенной операции происходит (или не происходит) поворот.

Если вкратце, то решение принимается путем операции
app_mask & topmost_controller_mask

где app_mask берется из Info.plist (либо application:supportedInterfaceOrientationsForWindow:), а topmost_controller_mask как результат вызова supportedInterfaceOrientations верхнего полноэкранного контроллера.

Также стоит принять во внимание следующие моменты:

  • так как значение app_mask действует глобально, следует изменять его осмотрительно
  • для временного отключения возможности поворота рекомендуется использовать shouldAutorotate, а не проводить манипуляции с маской в supportedInterfaceOrientations
  • если в AppDelegate используется метод application:supportedInterfaceOrientationsForWindow:, то значения из Info.plist будут проигнорированы
  • если в какой-то момент результатом конъюнкции будет 0, это приведет к исключению UIApplicationInvalidInterfaceOrientationException
  • если вы не переопределили методы supportedInterfaceOrientations и shouldAutorotate, по умолчанию контроллеры будут поддерживать все типы ориентации на устройствах iPad, а на iPhone — все, кроме PortraitUpsideDown
  • новый метод preferredInterfaceOrientationForPresentation позволяет указать предпочитаемую ориентацию контроллера при его отображении
  • shouldAutoRotateToInterfaceOrientation: больше не вызывается в iOS 6, однако вы должны по прежнему использовать его для поддержки устройств с прошлыми версиями iOS


Таковы изменения. Данный ход продиктован желанием Apple перенести ответственность за принятие решений о поддерживаемом положении экрана с каждого конкретного активного контроллера на контроллеры-контейнеры и само приложение.

Ключевые мысли от Apple (Session 236 from WWDC 2012) по этому поводу звучат следующим образом:
  • контроллеры должны стремиться к поддержке всех возможных режимов
  • дочерние контроллеры должны уметь отображаться в любом фрейме, указанном их родителем
  • приложение должно иметь возможность указать поддерживаемые типы ориентации (Info.plist либо application:supportedInterfaceOrientationsForWindow:)
  • при повороте только root или верхний полноэкранный контроллер будут опрошены


Что с этим делать


При разработке нового проекта, от которого требуется поддержка iOS 5 и более ранних версий, Apple рекомендует стараться эмулировать механизмы iOS 6:
  • в root или полноэкранном контроллере указывать полный список поддерживаемых ориентаций экрана
  • в child-контроллерах реализовывать поддержку всех необходимых ориентаций

Однако, что делать если необходимо мигрировать (желательно, с минимальными усилиями) на iOS 6 уже существующий проект, в котором решения о поворотах принимаются различными конечными контроллерами? Использование новых методов supportedInterfaceOrientations/shouldAutorotate рядом с shouldAutorotateToInterfaceOrientation ситуацию не спасет, если эти контроллеры не root и не top-most full-screen. Чтобы заставить контроллеры-контейнеры прислушиваться к мнению контролируемых можно воспользоваться следующими подходами.

1. Категория.
При помощи категории переопределить новые методы так, чтобы опрашивать соответствующие топ-контроллеры на предмет поворота. К примеру, для UINavigationController это может выглядеть так:

@implementation UINavigationController (RotationIOS6)

-(BOOL)shouldAutorotate
{
    return [self.topViewController shouldAutorotate];
}

-(NSUInteger)supportedInterfaceOrientations
{
    return [self.topViewController supportedInterfaceOrientations];
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
    return [self.topViewController preferredInterfaceOrientationForPresentation];
}
@end 


2. Наследование.
Реализовать то же, что и в пункте 1, но путем наследования от UINavigationController — когда нет необходимости глобально подвергать модификации сразу все UINavigationController-ы.

// CustomNavigationController.h

@interface CustomNavigationController : UINavigationController

@end


// CustomNavigationController.m

#import "CustomNavigationController.h"

@implementation CustomNavigationController

-(BOOL)shouldAutorotate
{
    return [self.topViewController shouldAutorotate];
}

-(NSUInteger)supportedInterfaceOrientations
{
    return [self.topViewController supportedInterfaceOrientations];
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
    return [self.topViewController preferredInterfaceOrientationForPresentation];
}

@end


3. Method swizzling.
Для любителей runtime и поклонников хардкора — используя swizzling, переопределить новые методы так, чтобы фактически использовать вызовы старого привычного метода shouldAutorotateToInterfaceOrientation:.

(код взят отсюда)

@implementation AppDelegate
void SwapMethodImplementations(Class cls, SEL left_sel, SEL right_sel) {
    Method leftMethod = class_getInstanceMethod(cls, left_sel);
    Method rightMethod = class_getInstanceMethod(cls, right_sel);
    method_exchangeImplementations(leftMethod, rightMethod);
}

+ (void)initialize {
    if (self == [AppDelegate class]) {
#ifdef __IPHONE_6_0
        SwapMethodImplementations([UIViewController class], @selector(supportedInterfaceOrientations), @selector(sp_supportedInterfaceOrientations));
        SwapMethodImplementations([UIViewController class], @selector(shouldAutorotate), @selector(sp_shouldAutorotate));
#endif
    }
}
@end

@implementation UIViewController (iOS6Autorotation)
#ifdef __IPHONE_6_0
/*
 * We've swizzled the new iOS 6 autorotation callbacks onto their iOS 5 and iOS 4 equivalents
 * to preserve existing functionality.
 *
 */
- (BOOL)sp_shouldAutorotate {
    BOOL shouldAutorotate = YES;
    if ([self respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) {
        NSUInteger mask = 0;
        if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationPortrait]) {
            mask |= UIInterfaceOrientationMaskPortrait;
        }
        if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationLandscapeLeft]) {
            mask |= UIInterfaceOrientationMaskLandscapeLeft;
        }
        if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationLandscapeRight]) {
            mask |= UIInterfaceOrientationMaskLandscapeRight;
        }
        if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationPortraitUpsideDown]) {
            mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
        }
        if (mask == 0) {
            // Shouldn't autorotate to *any* orientation.
            shouldAutorotate = NO;
        }
    } else {
        // This actually calls the original method implementation
        // instead of recursively calling into this method implementation.
        shouldAutorotate = [self sp_shouldAutorotate];
    }
    return shouldAutorotate;
}

- (NSUInteger)sp_supportedInterfaceOrientations {
    NSUInteger mask = 0;
    
    /*
     * In iOS 6, Apple dramatically changed the way autorotation works.
     * Rather than having each view controller respond to shouldAutorotateToInterfaceOrientation:
     * to specify whether or not it could support a particular orientation, the responsibility was
     * shifted to top-level container view controllers. That means UINavigationController becomes
     * responsible for declaring whether or not an orientation is supported. Since our app
     * has logic for how to autorotate on a per view controller basis, we call through to the
     * swizzled version of supportedInterfaceOrientations for the topViewController.
     *
     */
    if ([self isKindOfClass:[UINavigationController class]]) {
        return [[(UINavigationController *)self topViewController] supportedInterfaceOrientations];
    }
    
    if ([self respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) {
        if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationPortrait]) {
            mask |= UIInterfaceOrientationMaskPortrait;
        }
        if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationLandscapeLeft]) {
            mask |= UIInterfaceOrientationMaskLandscapeLeft;
        }
        if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationLandscapeRight]) {
            mask |= UIInterfaceOrientationMaskLandscapeRight;
        }
        if ([self shouldAutorotateToInterfaceOrientation:UIInterfaceOrientationPortraitUpsideDown]) {
            mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
        }
    } else {
        // This actually calls the original method implementation
        // instead of recursively calling into this method implementation.
        mask = [self sp_supportedInterfaceOrientations];
    }
    return mask;
}
#endif
@end

В данном случае больше не потребуется вообще никаких изменений существующего кода — магия runtime сделает свое дело. Однако, как бы заманчиво это не выглядело, данный код категорически не рекомендуется к использованию (узнать, почему).

В моем случае, удобнее всего оказалось воспользоватся категориями.

Надеюсь, изложенный материал кому-то пригодится и поможет сэкономить самый ценный ресурс разработчика — время :)

Полезные ссылки:
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 15
    0
    Прошу прощения, возможно я что-то не уловил, не могли бы вы привести конкретный пример для случая когда в универсальном приложении для iPhone запретить смену ориентации девайса, а для iPad, само собой, разрешить?
      +1
      if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
      // возвращаем значения для iPad
      }
      else {
      // и не iPad
      }
        +2
        Если только для iOS6 и для всего приложения, то достаточно просто в Targets — Summary — iPhone/iPod Deployment Info — Supported Interface Orientations и iPad Deployment Info — Supported Interface Orientations выбрать нужные поддерживаемые ориентации для соответствующих устройств.

        В других случаях — нужно воспользоваться одним из приведенных в статье методов и в контроллерах, где необходимо ограничить/разрешить смену ориентации, использовать проверку типа устройства (например, так, как предложил Stam).
        –1
        За разрешение UIInterfaceOrientationPortraitUpsideDown на айфоне/айподе предлагаю расстреливать — ни разу не сталкивался с ситуацией, в которой это положение имело бы смысл, но очень часто матерюсь, а иногда вовсе сношу приложения, которые позволяют экрану переворачиваться «кверхногами» — очень неудобно пользоваться ими лёжа.
          +4
          Бывает ситуация, когда штекер (например зарядка в машине) не позволяет держать устройство в обычном UIInterfaceOrientationPortrait режиме. В таком случае UIInterfaceOrientationPortraitUpsideDown позволяет воспользоваться приложением. Я бы так критично не относился к дополнительному перевороту, а просто блокировал бы изменения режима, благо это легко включается/выключается.
            0
            К ответу makoomazan еще добавлю: в iPhone 5 разъем для наушников перенесли на нижнюю грань, что увеличивает количество ситуаций, когда UIInterfaceOrientationPortraitUpsideDown вас выручит.
              0
              В машине часто требуется, когда на зарядке айфон.
                0
                Ок, спасибо за найденные варианты использования айфона «кверхногами».

                Новый вариант: «За неотключаемое UIInterfaceOrientationPortraitUpsideDown в программах-чатиках и программах-читалках (которые изначально не предназначены для использования в кредле за рулём автомобиля) на айфоне/айподе предлагаю лишать сладкого. Программы-навигаторы имеют право переворачиваться, как им вздумается» :)
              +1
              Автор, вы уверены что там именно логическое умножение?
              app_mask && topmost_controller_mask
              Детально тему не изучал, но из изложенного вами там никак && не может быть, это было бы лишено всякого смысла. Должно быть именно bitwise and
              app_mask & topmost_controller_mask
                0
                Вы правы. Хотя документация и говорит «At any given time, the mask of the topmost view controller is logically ANDed with the app’s mask to determine what orientations are permitted» — действительно, смысл тут есть только при использовании побитовой операции.

                В момент написания в 4 часа ночи мой сонный мозг не придал этому значения. Спасибо за замечание, внес исправления в статью.
                  0
                  > Хотя документация и говорит

                  Они там пишут про «the mask… is logically ANDed», mask это уже указание на побитовые операции, добавление bitwise это уже будет как масло масляное.
                0
                За статью спасибо, но мне кажется, можно было бы изложить как-то покороче. Более в виде справочника и best practices. Или код разместить выше — чтобы его можно было быстро скопипастить себе в проект, а уж если захочется теории — почитать дальше.

                Например, на том же StackOverflow я часто даже не читаю полностью вопрос, а сразу начинаю читать ответ для экономии времени.
                  0
                  StackOverflow достаточно часто дает fast fix'ы или даже workaround'ы и не претендует на полноту изложения вопроса.

                  Поэтому спасибо автору за такое полное описание проблемы и несколько решений!
                  0
                  Мне в iOS почти всё нравится больше и кажется удобнее, чем в Android, но только не Autorotation. Мой личный список грехов Apple:

                  — iTunes;
                  — developer.apple.com/membercenter;
                  — autorotation.
                    0
                    У iOS 6 наблюдается замечательный баг: приложение может только Landscape, запускает авторизацию Game Center, который в свою очередь хочет Portrait, на 5 GC поворачивался, на 6 приложение падает.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое