Если вы создаете приложения для гаджетов от Apple, то наверняка в курсе, что недавно произошло обновление iOS до версии 6.
Наравне с другими новыми функциями Apple внесла изменения в механизм autorotation.
На всякий случай напомню, что autorotation — это механизм, позволяющий использовать устройство как в портретной (вытянутой в высоту), так в альбомной (растянутой в ширину) ориентации, а также изменять эту ориентацию при повороте устройства.
Если в вашем приложении контент отображается в обеих ориентациях (а особенно если на некоторых экранах вам нужно запретить поворот) — готов поспорить, что у вас уже возникли некоторые вопросы.
Если же вы не используете функцию изменения ориентации экрана — разницы могли и не заметить. Однако знание того, как в 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 значение, производя операцию конъюнкции (logical AND) со списком глобально поддерживаемых приложением ориентаций (берется из 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 сделает свое дело. Однако, как бы заманчиво это не выглядело, данный код категорически не рекомендуется к использованию (узнать, почему).
В моем случае, удобнее всего оказалось воспользоватся категориями.
Надеюсь, изложенный материал кому-то пригодится и поможет сэкономить самый ценный ресурс разработчика — время 🙂
Полезные ссылки:
ссылка на оригинал статьи http://habrahabr.ru/post/155969/
Добавить комментарий