iOS屏幕轉屏實現總結

在最近的開發中測試提交了個Bug:"打開視頻播放并將視頻橫屏播放,將App退到后臺,通過widget入口打開App,播放的視頻關閉,結果App主頁面顯示橫屏",這個問題讓我有看了下iOS設置轉屏的相關內容,個人覺得有必要總結下,將結果落實在筆頭上有助于記憶!

轉屏的實現機制

  • 轉屏基礎

加速計是實現iOS轉屏的基礎。依賴加速計才可以判斷出設備當前的方向,通過加速計檢測設備方向變化,將會發出UIDeviceOrientationDidChangeNotification這個通知,通知App設備方向發生了變化,注冊這個通知后在通知對應的方法中處理轉屏后的UI相關操作即可。

圖片
  1. 設備旋轉的時候UIKit會受到轉屏事件的通知
  2. UIKit通過AppDelegate通知當前的Window,并設置支持的屏幕方向;
  3. window通知ViewController的轉屏事件,判斷該viewController所支持的旋轉方向,完成旋轉;(如果頁面結構為viewControllerNavigationUITabbarController判斷有差異,后面會介紹)
  4. 如果存在模態彈出的viewController的話,系統則會根據此ViewController的設置來判斷是否要進行旋轉;

UIDeviceOrientationDidChangeNotification這個通知是設備的物理方向發生變化時會被發送,有時手機屏幕頁面沒有變化旋轉的需求,但是此通知仍被發送,所以這時會引起一些問題

  • 加速計判斷屏幕方向
self.motionManager = [[CMMotionManager alloc] init];
if (![self.motionManager isGyroAvailable]) {
  NSLog(@"CMMotionManager 陀螺儀不可用");
}
else{
  // 3.設置陀螺儀更新頻率,以秒為單位
  self.motionManager.gyroUpdateInterval = 0.1;
  // 4.開始實時獲取
  [self.motionManager startGyroUpdatesToQueue:[[NSOperationQueue     alloc] init] withHandler:^(CMGyroData * _Nullable gyroData, NSError * _Nullable error) {
            //獲取陀螺儀數據
            CMRotationRate rotationRate = gyroData.rotationRate;
            NSLog(@"CMMotionManager 加速度 == x:%f, y:%f, z:%f", rotationRate.x, rotationRate.y, rotationRate.z);
        }];
   }

轉屏相關枚舉值:

一共有三個枚舉值,下面就一一簡單的介紹下

  • UIDeviceOrientation:

UIDeviceOrientation指的是硬件設備當前的旋轉方向,判斷設備方向以Home鍵作為參考,這個枚舉在UIDevive.h中。

  • 枚舉內容
typedef NS_ENUM(NSInteger, UIDeviceOrientation) {
    UIDeviceOrientationUnknown,
    UIDeviceOrientationPortrait,            // Device oriented vertically, home button on the bottom
    UIDeviceOrientationPortraitUpsideDown,  // Device oriented vertically, home button on the top
    UIDeviceOrientationLandscapeLeft,       // Device oriented horizontally, home button on the right
    UIDeviceOrientationLandscapeRight,      // Device oriented horizontally, home button on the left
    UIDeviceOrientationFaceUp,              // Device oriented flat, face up
    UIDeviceOrientationFaceDown             // Device oriented flat, face down
} API_UNAVAILABLE(tvOS);
  • 獲取設備旋轉方向的方法
[UIDevice currentDevice].orientation
  • orientation這個屬性是只讀的,設備方向是只能取值,不能設置值
@property(nonatomic,readonly) UIDeviceOrientation orientation;
  • 監聽設備方向變化
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleDeviceOrientationChange) name:UIDeviceOrientationDidChangeNotification object:nil];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
- (BOOL)handleDeviceOrientationChange{
    UIDeviceOrientation *deviceOrientation = [UIDevice currentDevice].orientation
    //根據設備方向處理不同的情況
    //省略.....
}

APP可以選擇性的是否接收UIDeviceOrientationDidChangeNotification通知。
相關屬性:

// 是否已經開啟了設備方向改變的通知
@property(nonatomic,readonly,getter=isGeneratingDeviceOrientationNotifications) BOOL generatesDeviceOrientationNotifications;
// 開啟接收接收 UIDeviceOrientationDidChangeNotification 通知
- (void)beginGeneratingDeviceOrientationNotifications;
// 結束接收接收 UIDeviceOrientationDidChangeNotification 通知
- (void)endGeneratingDeviceOrientationNotifications;
在 app 代理里面結束接收 設備旋轉的通知事件, 后續的屏幕旋轉監聽都會失效
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 結束接收接收 UIDeviceOrientationDidChangeNotification 通知
    [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
    return YES;
}
  • UIInterfaceOrientation 頁面的顯示方向:

UIInterfaceOrientation指的是頁面當前旋轉的方向,頁面方向是可以設置的,在系統鎖屏按鈕開啟的狀態下依舊可以通過強制轉屏來實現屏幕旋轉。這個枚舉值定義在UIApplication.h中。

  • 枚舉類型
typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
UIInterfaceOrientationUnknown = UIDeviceOrientationUnknow, UIInterfaceOrientationPortrait = UIDeviceOrientationPortrait,
UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
}

通過UIInterfaceOrientationUIDeviceOrientation對比發現兩者之間大部分的枚舉值是可以對應上的,只有以下兩個枚舉值是相反的:

UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft

當設備向左轉時屏幕是需要向右轉的,當設備向右轉時屏幕是需要向左轉的;

  • 獲取頁面方向的方式

iOS 8之前

//讀取UIViewController的interfaceOrientation屬性
@property(nonatomic,readonly) UIInterfaceOrientation

iOS 13之前,獲取狀態條方向

[UIApplication sharedApplication].statusBarOrientation

iOS13之后,獲取狀態條方向

[UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation;

獲取頁面方向的方法封裝

- (UIInterfaceOrientation)currentInterfaceOrientation
{
    if (@available(iOS 13.0 , *)) {
        return [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation;
    }
    return [UIApplication sharedApplication].statusBarOrientation;
}
  • 監聽頁面方向變化

在iOS13之前使用一下消息通知Key來注冊監聽,通過監聽狀態條的方向變化來監聽頁面方向變化

UIApplicationWillChangeStatusBarOrientationNotification
UIApplicationDidChangeStatusBarOrientationNotification

其他消息通知Key

UIApplicationStatusBarOrientationUserInfoKey
UIApplicationWillChangeStatusBarFrameNotification
UIApplicationDidChangeStatusBarFrameNotification
UIApplicationStatusBarFrameUserInfoKey

在iOS13以后以上的所有key雖然都已經DEPRECATED了,但仍然可以使用,但是蘋果建議使用viewWillTransitionToSize:withTransitionCoordinator:來替代頁面方向變化監聽。

  • UIInterfaceOrientationMask頁面方向

UIInterfaceOrientationMask是iOS6之后增加的一種枚舉,他是一個為了實現支持多種屏幕方向UIInterfaceOrientation而定義的類型。這個枚舉值也定義在UIApplication.h中。

  • 枚舉類型
typedef NS_OPTIONS(NSUInteger,UIInterfaceOrientationMask) {
  UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),
  UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),
  UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),
  UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),
  UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
  UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),
  UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
}

轉屏相關方法介紹

  • AppDelegate中的相關方法
//iOS6在UIApplucationDelegate中提供這個方法,用于指定UIWindow的界面屏幕方向
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
    return UIInterfaceOrientationMaskPortrait;
}
  • 根視圖中的轉屏相關方法

常用方法

方法1:設置決定當前頁面是否可以自動旋轉,YES為支持旋轉,NO為不支持旋轉;如果返回NO,則其他轉屏相關方法將不會再被調用。

- (BOOL)shouldAutorotate

方法2:設置頁面支持的旋轉方向。
iPhone上默認返回UIInterfaceOrientationMaskAllButUpsideDown;

iPad上默認返回UIInterfaceOrientationMaskAll;

- (UIInterfaceOrientationMask)supportedInterfaceOrientations

方法3:設置進入頁面時默認顯示的方向

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentatio

方法4:重寫viewWillTransitionToSize: withTransitionCoordinator:方法來處理旋轉后的事件(iOS8之后可以使用此方法,如需要適配iOS8以前的設備仍需要注冊轉屏監聽)

-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    if(size.width > size.height) {
    //橫屏處理
    } else {
    //豎屏處理
    }
}

方法1、2、3為iOS6以后常用方法,方法4為iOS8以后監聽處理轉屏完成的方法;

**其他方法:**
//頁面嘗試旋轉到與設備屏幕方向一致,當interface orientation和device orientation方向不一致時,希望通過重新指定 interface orientation 的值,立即實現二者一致
+ (void)attemptRotationToDeviceOrientation
//應用將要使用界面支持的方向,或者將要自動旋轉 (在iOS6以后被禁用,要兼容iOS 6還是需要實現這個方法)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
//獲取用戶界面的方向 (方法在iOS8被禁用)
@property(nonatomic,readonly) UIInterfaceOrientation interfaceOrientation 
//頁面將要旋轉(iOS8以后已經失效)
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
//頁面已經旋轉完成(iOS8以后已經失效)
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
//將要動畫旋轉到用戶界面(iOS8以后已經失效)
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
//界面切換到一半時的回調(iOS5以后已經失效)
- (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
- (void)didAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
- (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration

注:再iOS8以后willRotateToInterfaceOrientation和willAnimateRotationToInterfaceOrientation的替代方法為viewWillTransitionToSize:withTransitionCoordinator

  • attemptRotationToDeviceOrientation 使用說明

該方法的使用場景是 interface orientation和device orientation 不一致,但希望通過重新指定 interface orientation 的值,立即實現二者一致;如果這時只是更改了支持的 interface orientation 的值,沒有調用attemptRotationToDeviceOrientation,那么下次 device orientation 變化的時候才會實現二者一致,關鍵點在于能不能立即實現。

假設當前的 interface orientation 只支持 Portrait。
如果 device orientation 變成 Landscape,那么 interface orientation 仍然顯示 Portrait;
如果這時我們希望 interface orientation 也變成和 device orientation 一致的 Landscape,
需要先將 supportedInterfaceOrientations 的返回值改成Landscape,然后調用 attemptRotationToDeviceOrientation方法,系統會重新詢問支持的 interface orientation,已達到立即更改當前 interface orientation的目的

  • 系統版本區別

iOS 6 and above

iOS6以后再控制器中需要實現的方法

1、shouldAutorotate
2、supportedInterfaceOrientations
3、preferredInterfaceOrientationForPresentation

iOS5 and before

iOS5之前控制器中需要實現的方法

shouldAutorotateToInterfaceOrientation

決定頁面方向的因素

  • 屏幕旋轉控制的優先級

Device Orientation控制配置 = AppDelegate中window配置 >根視圖控制器配置 >最上層視圖控制器配置

  • 屏幕旋轉控制設置
  1. Device Orientation方向配置:

在Xcode中依次打開:General→Deployment Info→Device Orientation 設置支持的旋轉方向,下面的圖片是默認設置。

圖片

注意:如果這四個選項都不選的話,Device Orientation 的值為默認值。在這里設置值與 info.plist中設置的值是同步的,也就是說修改其中的一個,另一個的值也會相應的變化。

  1. Info.plist中方向配置:

上面說了Info.plist中Info.plist中的Supported interface orientation的值與Device Orientation的值是同步的,所以在我們設置完Device Orientation再到info.plist中查看Supported interface orientation的值,發現是一樣的

圖片

注意:在1和2中的設置都是全局控制的

  1. 在UIApplication中window支持方向設置:

iOS6的UIApplicationDelegate提供了下述方法,能夠指定 UIWindow 中的界面的屏幕方向,該方法默認值為 Info.plist 中配置的 Supported interface orientations 項的值

//設置window只支持豎屏
- (NSUInteger)application:(UIApplication *)application  supportedInterfaceOrientationsForWindow:(UIWindow *)window 
{
  return  UIInterfaceOrientationMaskPortrait;
}

window為AppDelegate中所持有的唯一的,并且是全局的,所以在方法中設置的屏幕方向也是全局有效的。

  1. 視圖控制器中的方向配置:

在window的根視圖rootViewController和viewController是通過modal模態彈出方式顯現的時候頁面旋轉的相關方法才會被調用,當前controller及其所有childViewController都在此作用范圍內。如果需求是想控制單個界面支持轉屏,就需要在當前視圖控制器中重寫以下方法了

//Interface的方向是否會跟隨設備方向自動旋轉,如果返回NO
- (BOOL)shouldAutorotate {
    return YES;
}
//返回直接支持的方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
    return UIInterfaceOrientationMaskPortrait;
}
//返回最優先顯示的屏幕方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationPortrait;
}

這里涉及到的控制器有以下三個:

UITabbarViewController,UINavigationBarController ,UIViewController

當頁面嵌套三種控制器使用時,其優先級為:

UITabbarViewController>UINavigationBarController >UIViewController

說明:

* 如果是單一的`viewController,`其轉屏方向取決于此控制器的轉屏方法中的配置;
* 如果是`UINavigationBarController + UIViewController`,其轉屏方向取決于`UINavigationBarController`導航控制器的轉屏方法配置;

如果是UITabbarViewController + UINavigationBarController + UIViewController其轉屏方向取決于UIViewController導航控制器的轉屏方法配置;

總結:

* 如果`viewController`存在根視圖控制器,則`viewController`的轉屏相關方法就不會再被調用,在`UINavigationBarController + UIViewController`結構下`UINavigationBarController`的方法會被調用,`UIViewController`的方法失效;在`UITabbarViewController + UINavigationBarController + UIViewController`結構下`UITabbarViewController`的方法會被調用,其他兩個的方法失效。所以如果期望使用當前`viewController`控制器來決定是否轉屏就會產生問題(下面會說怎樣由具體VC來控制),因為這個方法被根視圖控制器攔截了!
  • 如何決定屏幕最終支持的方向

決定界面最后支持的屏幕方向的是 target&plist ∩ AppDeleagte中window設置 ∩ 視圖控制器設置 這三個位置的交集。如果這個交集為空,就會拋出UIApplicationInvalidInterfaceOrientation異常崩潰。

如何使用具體VC來控制

可以創建UITabbarViewController 、 UINavigationBarController 、 UIViewController 的子類或分類,在子類或分類中重寫以下代碼

  • 只存在單獨的viewController時如何設置
- (BOOL)shouldAutorotate{
    return NO;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskAll;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
    return UIInterfaceOrientationPortrait;
}
  • 控制器結構為UINavigationController+UIViewController時如何設置

需要在導航控制器UINavigationController下設置

//返回導航控制器的頂層視圖控制器的自動旋轉屬性,因為導航控制器是以棧的原因疊加VC的
-(BOOL)shouldAutorotate{
    return self.topViewController.shouldAutorotate;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return self.topViewController.supportedInterfaceOrientations;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
    return self.topViewController.preferredInterfaceOrientationForPresentation;
}
  • 控制器結構為UITabbarViewController+UINavigationController+UIViewController時如何設置

需要在UITabbarViewController中設置

-(BOOL)shouldAutorotate
{
    return self.selectedViewController.shouldAutorotate;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations 
{
    return self.selectedViewController.supportedInterfaceOrientations;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
     return self.selectedViewController.preferredInterfaceOrientationForPresentation;
}
  • 使用模態視圖

使用模態modal彈出的viewController不在受到根視圖的控制,具體的設置和普通視圖器代碼相同。

強制轉屏

  • 方法一

私有方法,無法直接調用

[[UIDevice currentDevice] setOrientation:UIInterfaceOrientationPortrait]; 

可以間接調用,上線未被拒

[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationUnknown] forKey:@"orientation"];
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationPortrait] forKey:@"orientation"];
 [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationUnknown] forKey:@"orientation"];
 [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationLandscapeLeft] forKey:@"orientation"];
  • 方法二

也是調用私有方法,

- (void)setScreenOrientation:(UIInterfaceOrientation)orientation
{
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
    SEL selector = NSSelectorFromString(@"setOrientation:");
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
    [invocation setSelector:selector];
    [invocation setTarget:[UIDevice currentDevice]];
    int val = orientation;
    [invocation setArgument:&val atIndex:2];  
    [invocation invoke];
    }
}
  • 方法三

旋轉view的transform

可以通過旋轉view的transform屬性達到強制旋轉屏幕方向的目的,但是這樣會有很多問題,以及適配問題,AlertView方向,狀態條方向等。

//設置statusBar
[[UIApplication sharedApplication] setStatusBarOrientation:orientation];  
//計算旋轉角度
float arch;
if (orientation == UIInterfaceOrientationLandscapeLeft)
    arch = -M_PI_2;
else if (orientation == UIInterfaceOrientationLandscapeRight)
    arch = M_PI_2;
else
    arch = 0;
//對根視圖控制器進行強制旋轉
self.navigationController.view.transform = CGAffineTransformMakeRotation(arch);
self.navigationController.view.bounds = UIInterfaceOrientationIsLandscape(orientation) ? CGRectMake(0, 0, SCREEN_HEIGHT, SCREEN_WIDTH) : initialBounds

注意:

  1. [[UIApplication sharedApplication] setStatusBarOrientation這個方法在iOS9以后已經失效,使用會有警告。如果將shouldAutorotate設置為YES,setStatusBarOrientation方法設置無效,只有shouldAutorotate設置為NO,才會起作用。

開發中的問題

  • 在系統鎖屏按鈕開啟,播放器橫屏播放,進入后臺,再回前臺橫屏變豎屏,需求仍是橫屏

解決:在App從后臺返回前臺后EnterForeground時判斷當前頁面方向,如果是橫屏將頁面強制橫屏。

  • 在6p,7p一些支持桌面橫屏的設備上,橫屏啟動App頁面橫屏顯示,需求是啟動豎屏

解決:在targe的Device Orientation中只設置portrait一項

圖片
  • info.plist中設置Initial interface orientation未起作用

解決:原因未找到

參考文獻

iOS屏幕旋轉及其基本適配方法

iOS屏幕旋轉問題總結

IOS Orientation, 想怎么轉就怎么轉

iOS Rotation

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。