- PVSM.RU - https://www.pvsm.ru -
Привет, друзья!
Если вы создаете приложения для гаджетов от Apple, то наверняка в курсе, что недавно произошло обновление iOS до версии 6.
Наравне с другими [1] новыми функциями Apple внесла изменения в механизм autorotation.
На всякий случай напомню, что autorotation — это механизм, позволяющий использовать устройство как в портретной (вытянутой в высоту), так в альбомной (растянутой в ширину) ориентации, а также изменять эту ориентацию при повороте устройства.
Если в вашем приложении контент отображается в обеих ориентациях (а особенно если на некоторых экранах вам нужно запретить поворот) — готов поспорить, что у вас уже возникли некоторые вопросы [2].
Если же вы не используете функцию изменения ориентации экрана — разницы могли и не заметить. Однако знание того, как в iOS6 работает autorotation, в любом случае будет полезно и пригодится в будущем.
Устройства под управлением iOS поддерживают 4 возможных ориентации экрана, описываемых соответствующими системными константами:
В 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 метод shouldAutorotateToInterfaceOrientation объявлен устаревшим (deprecated), а за логику работы autorotation отвечают два других — supportedInterfaceOrientations и shouldAutorotate.
При изменения положения устройства (или когда контроллер презентуется модально) система опрашивает самый верхний полноэкранный контроллер (top-most full-screen view controller). При этом сначала происходит вызов shouldAutorotate, а затем (только в случае возврата значения YES) — вызов supportedInterfaceOrientations для получения битовой маски, описывающей поддерживаемые положения. Например, следущий код может использоваться для подержки обычной портретной и обеих альбомных ориентаций.
- (NSInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait |
UIInterfaceOrientationMaskLandscapeLeft |
UIInterfaceOrientationMaskLandscapeRight;
}
Далее, система использует полученное от supportedInterfaceOrientations значение, производя операцию конъюнкции [3] (logical AND) со списком глобально поддерживаемых приложением ориентаций (берется из Info.plist либо как результат метода AppDelegate application:supportedInterfaceOrientationsForWindow:). По итогам проведенной операции происходит (или не происходит) поворот.
Если вкратце, то решение принимается путем операции
app_mask && topmost_controller_mask
где app_mask берется из Info.plist (либо application:supportedInterfaceOrientationsForWindow:), а topmost_controller_mask как результат вызова supportedInterfaceOrientations верхнего полноэкранного контроллера.
Также стоит принять во внимание следующие моменты:
Таковы изменения. Данный ход продиктован желанием Apple перенести ответственность за принятие решений о поддерживаемом положении экрана с каждого конкретного активного контроллера на контроллеры-контейнеры и само приложение.
Ключевые мысли от Apple (Session 236 from WWDC 2012) по этому поводу звучат следующим образом:
При разработке нового проекта, от которого требуется поддержка iOS 5 и более ранних версий, Apple рекомендует стараться эмулировать механизмы iOS 6:
Однако, что делать если необходимо мигрировать (желательно, с минимальными усилиями) на 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 [4], переопределить новые методы так, чтобы фактически использовать вызовы старого привычного метода shouldAutorotateToInterfaceOrientation:.
(код взят отсюда [5])
@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 сделает свое дело. Однако, как бы заманчиво это не выглядело, данный код категорически не рекомендуется к использованию (узнать, почему) [6].
В моем случае, удобнее всего оказалось воспользоватся категориями.
Надеюсь, изложенный материал кому-то пригодится и поможет сэкономить самый ценный ресурс разработчика — время :)
Полезные ссылки:
Автор: xZenon
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/17884
Ссылки в тексте:
[1] другими: https://developer.apple.com/library/ios/#releasenotes/General/WhatsNewIniPhoneOS/Articles/iOS6.html#//apple_ref/doc/uid/TP40011812-SW1
[2] вопросы: http://stackoverflow.com/questions/12526054/autorotate-in-ios-6-has-strange-behaviour/12538622
[3] конъюнкции: http://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%8A%D1%8E%D0%BD%D0%BA%D1%86%D0%B8%D1%8F
[4] swizzling: http://en.wikipedia.org/wiki/Pointer_swizzling
[5] отсюда: https://gist.github.com/3725118
[6] (узнать, почему): http://stackoverflow.com/questions/5339276/what-are-the-dangers-of-method-swizzling-in-objective-c
[7] View Controller Programming Guide for iOS: http://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/RespondingtoDeviceOrientationChanges/RespondingtoDeviceOrientationChanges.html
[8] WWDC 2012, Session 236 «View Controller Evolution»: http://adcdownload.apple.com//wwdc_2012/wwdc_2012_session_pdfs/236__the_evolution_of_view_controllers_on_ios.pdf
[9] Method Swizzling: http://cocoadev.com/wiki/MethodSwizzling
Нажмите здесь для печати.