本文基于 Swift 3.x,由于 Swift 4.x 在語法規則上有較大變動,后續出一個 Swift 4.x 版本, Demo 工程在最下面。
前言
我相信iOS的屏幕旋轉問題一直困擾著大多數的APP開發者,遇到界面需要旋轉,特別是界面之間的關聯性很強,幾個視圖控制器又是Push又是Present,然后又交叉Push、Present...說到這里,腦海里就浮現出未找到解決方案時,想拍案而起抓狂的場景。

案例場景
圖有點大,可以打開一個新標簽放大查看,我們項目APP的一個大概的結構圖,主要指示了一下涉及到旋轉屏的視圖控制器,以及各個控制器之間的關系,是Push出來的還是Present出來的。
簡單描述一下場景:
- 主視圖控制器是一個繼承自 UITabBarController 的視圖控制器。
- 底部有四個Tab,四個Tab分別指向繼承自 UINavigationController 的視圖控制器作為根視圖。
- 通常情況下,都是豎屏,四個Tab的部分界面中都有跳播放器視圖控制器的入口。
- 進播放器時,有兩種方式進入,豎屏 or 橫屏。
- 第一次是默認豎屏,之后進入時,由用戶最后退出播放器時的閱讀方向來決定。
- 播放器中有四個菜單和一個評論輸入框。
- 點擊 評論輸入框,彈出一個可輸入評論的視圖控制器,以 present 的形式彈出,會覆蓋在播放器之上,并且能看到后面的播放器內容。方向與當前閱讀器的方向一致。
- 點擊 目錄,以 push 的方式打開目錄頁。目錄頁方向與播放器方向一致。(之前的需求是目錄頁要以豎屏的方式出現,當然,這個也可以實現,下面會說解決方案)
- 點擊 旋轉 菜單,切換播放器方向,豎屏 -> 橫屏,or 橫屏 -> 豎屏。
- 用戶在輸入評論之后,點擊右邊或者鍵盤的的 發送 按鈕,會先判斷當前用戶的登錄狀態,如果未登錄或者登錄信息失效,會 present 一個 豎屏 的 登錄界面。
- 登錄界面 同樣包裝在一個 UINavigationController 之中,用戶未注冊時還可以 push 到一個 注冊 界面,同樣也是豎屏,第三方登錄方式有 微信,QQ,微博 等。
- 播放器可以被外部APP調起,諸如 Safari瀏覽器 或者 QQ瀏覽器。(為什么要說到這一點,是因為當用在在這些外部APP中調起播放器時,用戶手持手機的方向會直接影響到調起之后,播放器的方向,處理不好的話就會錯亂,比如之前播放器時橫屏,從外部APP調起時,手機又是豎屏。)
了解一點基礎知識
在講解我的處理方案之前,我想先跟大家介紹一下Apple的官方文檔關于旋轉屏時的處理機制。
在Apple Documentation 中 關于 UIViewController 的介紹中,簡要提到過旋轉屏時,UIKit會干一些什么事以及你該怎么處理。我提取其中的部分簡單翻譯了一下。如下:
Handling View Rotations
As of iOS 8, all rotation-related methods are deprecated. Instead, rotations are treated as a change in the size of the view controller’s view and are therefore reported using the viewWillTransition(to:with:) method. When the interface orientation changes, UIKit calls this method on the window’s root view controller. That view controller then notifies its child view controllers, propagating the message throughout the view controller hierarchy.
從iOS8開始,所有旋轉相關的方法都被廢棄。旋轉被視為是視圖控制器的view的大小的改變并在viewWillTransition(to:with:) 方法中反饋給視圖控制器。當界面方向發生改變,UIKit會在窗口的根視圖控制器中調用此方法,然后根視圖控制器再通知它所管理的其他子視圖控制器。此消息將在整個視圖控制器棧中傳播貫穿。
In iOS 6 and iOS 7, your app supports the interface orientations defined in your app’s Info.plist file.
在iOS6和iOS7中,你的程序所支持的界面方向由程序的info.plist文件中定義的參數決定。
A view controller can override the supportedInterfaceOrientationsmethod to limit the list of supported orientations.Typically, the system calls this method only on the root view controller of the window or a view controller presented to fill the entire screen;
一個視圖可以通過重寫 supportedInterfaceOrientations 來控制支持的方向。通常情況下,系統只在window的rootViewController和一個充滿全屏的模態(presented view controller)視圖中調用此方法。
child view controllers use the portion of the window provided for them by their parent view controller and no longer participate directly in decisions about what rotations are supported.
子視圖不直接參與旋轉方向的決策,直接由它們的父視圖決定。
The intersection of the app's orientation mask and the view controller's orientation mask is used to determine which orientations a view controller can be rotated into.
程序支持的方向和視圖控制器支持的方向的交集被用來決定視圖控制器應該旋轉到哪個方向。
You can override the preferredInterfaceOrientationForPresentation for a view controller that is intended to be presented full screen in a specific orientation.
你可以為一個準備present成一個全屏的模態視圖控制器通過重寫 preferredInterfaceOrientationForPresentation 來指定特定的方向。
When a rotation occurs for a visible view controller, the willRotate(to:duration:), willAnimateRotation(to:duration:), and didRotate(from:) methods are called during the rotation. The viewWillLayoutSubviews() method is also called after the view is resized and positioned by its parent. If a view controller is not visible when an orientation change occurs, then the rotation methods are never called. However, the viewWillLayoutSubviews() method is called when the view becomes visible. Your implementation of this method can call the statusBarOrientation method to determine the device orientation.
對于一個可見的視圖控制器,當旋轉發生時,這些方法willRotate(to:duration:), willAnimateRotation(to:duration:), 和 didRotate(from:) 會在旋轉過程中被調用,當視圖控制器的view被重新拉伸并被父視圖定位完成時,viewWillLayoutSubviews() 將被調用。如果一個視圖控制器在旋轉過程中處于不可見狀態,那么上面提到的三個方法不會被調用。然而,在視圖重新可見時,viewWillLayoutSubviews() 會被調用。你可以重寫此方法并在該方法中調用 statusBarOrientation 方法來決定設備的方向。
Note
At launch time, apps should always set up their interface in a portrait orientation. After the application(_:didFinishLaunchingWithOptions:) method returns, the app uses the view controller rotation mechanism described above to rotate the views to the appropriate orientation prior to showing the window.
注意
在程序應該在啟動時保持豎屏,等到application(_:didFinishLaunchingWithOptions:) 方法返回之后,程序再使用上面提到過的旋轉機制來合理的處理窗口視圖的旋轉。
額外說一下 statusBarOrientation 這個屬性:
The value of this property is a constant that indicates an orientation of the receiver's status bar. See UIInterfaceOrientation for details. Setting this property rotates the status bar to the specified orientation without animating the transition. If your app has rotatable window content, however, you should not arbitrarily set status-bar orientation using this method. The status-bar orientation set by this method does not change if the device changes orientation. For more on rotatable window views, see View Controller Programming Guide for iOS.
- 通過
UIApplication.shared.statusBarOrientation
獲取和設置,還有另外一個方法來設置這個屬性的值,可以傳遞動畫與否的參數,UIApplication.shared.setStatusBarOrientation(:, animated: )
,直接設置這個屬性值,相當于調用了該方法時傳入了animated: false
,即不使用任何動畫形式來改變狀態欄的方向。- 如果你的程序中的某個視圖控制器的界面是可旋轉的,那么你不應該隨意的去設置這個屬性,意圖改變狀態欄的方向,因為這將可能無效。(我就曾遇到過,邏輯都是從另外一個項目中照搬過來的,但是調用此方法時,死活不改變方向。當然,這跟你是否正確的返回
shouldAutorotate
有關系,下面會講到。)- 作為總結,如果你的當前視圖控制器的
shouldAutorotate
返回true
,則盡量不要再去調用UIApplication.shared.statusBarOrientation
了, 一是可能無效,二是statusBarOrientation
的方向會隨著你返回的supportedInterfaceOrientation
改變而自動改變。
正題
按照官方的說法,我打算一步一步的告訴大家,如何配置,如何編寫代碼,從最根部,到最外層。
-
首先,配置程序的info.plist配置文件,只勾選豎屏,這樣可以保證豎屏啟動界面 (即 LaunchScreen.storyboard 配置的程序默認啟動界面在任何情況下都豎屏啟動)。
程序Info.plist的配置 -
在
AppDelegate
中的配置:@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { ... func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return .allButUpsideDown } ... }
- 當然,如果你的程序支持 iPad ,可以返回
.all
來支持所有的方向。 - 一般情況下,返回
.allButUpsideDown
就夠了。 - 前面講到過,
UIKit
會取視圖控制器返回的值和當前返回的值,做一個交叉,取交叉值,所有這里返回最大范圍的支持方向。
- 當然,如果你的程序支持 iPad ,可以返回
-
自定義五個基類,分別是:
-
BaseTabBarController
,繼承自UITabBarControlelr
-
BaseNavViewController
,繼承自UINavigationController
-
BaseViewController
,繼承自UIViewController
-
BaseTableViewController
,繼承自UITableViewController
-
BaseCollectionViewController
,繼承自UICollectionViewController
這五個基類基本上覆蓋了程序的大部分需要的視圖控制器,如果您的程序中還有其他類型的視圖控制器,照著下面我所描述的原理,配置一下即可。
-
先寫上一個 swift 文件,為程序配置幾個默認配置的屬性,供全局使用,并配置一些相關拓展,下面會用到。
// 基礎視圖控制器的默認配置,涵蓋了跟旋轉屏、present時屏幕方向和狀態欄樣式有關系的常用配置 let kDefaultPreferredStatusBarStyle: UIStatusBarStyle = .default // 狀態欄樣式,默認使用系統的 let kDefaultPrefersStatusBarHidden: Bool = false // 狀態欄是否隱藏,默認不隱藏 let kDefaultShouldAutorotate: Bool = true // 是否支持屏幕旋轉,默認支持 let kDefaultSupportedInterfaceOrientations: UIInterfaceOrientationMask = .portrait // 支持的旋轉方向,默認豎屏 let kDefaultPreferredInterfaceOrientationForPresentation: UIInterfaceOrientation = .portrait // present時,打開視圖控制器的方向,默認豎屏 extension UIInterfaceOrientation { var orientationMask: UIInterfaceOrientationMask { switch self { case .portrait: return .portrait case .portraitUpsideDown: return .portraitUpsideDown case .landscapeLeft: return .landscapeLeft case .landscapeRight: return .landscapeRight default: return .all } } } extension UIInterfaceOrientationMask { var isLandscape: Bool { switch self { case .landscapeLeft, .landscapeRight, .landscape: return true default: return false } } var isPortrait: Bool { switch self { case . portrait, . portraitUpsideDown: return true default: return false } } }
-
-
再來添加另外一個 swift 文件,起名
UIViewController+Extension.swift
, 為UIViewController
添加一些通用配置。extension UIViewController { // 是否禁用導航欄的左滑手勢,默認不禁用 var isForbidInteractivePopGesture: Bool { return false } }
額呵,只有這么一個簡單的配置,為的是在播放器處于橫屏時,禁用導航控制器的左滑返回手勢,豎屏時正常可用。
為什么要禁用?。?!
因為上一個界面是豎屏!!而播放器也是被 Push 進來的。so!要么禁用,要么一觸發滑動,界面就立刻關閉了,體驗不好。
-
配置
BaseTabBarController
:class BaseTabBarController: UITabBarController { override var prefersStatusBarHidden: Bool { return selectedViewController?.prefersStatusBarHidden ?? kDefaultPrefersStatusBarHidden } override var preferredStatusBarStyle: UIStatusBarStyle { return selectedViewController?.preferredStatusBarStyle ?? kDefaultPreferredStatusBarStyle } override var shouldAutorotate: Bool { return selectedViewController?.shouldAutorotate ?? kDefaultShouldAutorotate } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return [selectedViewController?.supportedInterfaceOrientations ?? kDefaultSupportedInterfaceOrientations, preferredInterfaceOrientationForPresentation.orientationMask] } override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return selectedViewController?.preferredInterfaceOrientationForPresentation ?? kDefaultPreferredInterfaceOrientationForPresentation } }
BaseTabBarController 作為根視圖,需要把參數傳遞給它的子視圖。
注意:上面的代碼,重寫
supportedInterfaceOrientations
時,也取了preferredInterfaceOrientationForPresentation
的值并做了一個轉換,之所以這么處理,是因為很多情況下,我們會無意間返回與supportedInterfaceOrientations
不一致的方向,導致這種錯誤:UIApplicationInvalidInterfaceOrientation: preferredInterfaceOrientationForPresentation 'landscapeRight' must match a supported interface orientation: 'portrait'!
可以看出,系統要求我們返回的
supportedInterfaceOrientations
與preferredInterfaceOrientationForPresentation
至少要有可交叉的值,UIInterfaceOrientation
只能定義一個值,UIInterfaceOrientationMask
支持OptionSet
協議 可返回一個數組,因此可以是多個值,所以可做如上處理,避免你沒有重寫preferredInterfaceOrientationForPresentation
由系統返回的默認值 或者 你重寫了,但是由于代碼邏輯錯誤,返回了一個與supportedInterfaceOrientations
方向不一致的值。
-
配置
BaseNavViewController
:class BaseNavViewController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() interactivePopGestureRecognizer?.delegate = self // 切記不要放在構造方法中配置,因為那時的 interactivePopGestureRecognizer 可能是 nil } override var shouldAutorotate: Bool { if let presentedController = topViewController?.presentedViewController, presentedController.isBeingPresented { return presentedViewController?.shouldAutorotate ?? kDefaultShouldAutorotate } if let presentedController = topViewController?.presentedViewController, presentedController.isBeingDismissed { return topViewController?.shouldAutorotate ?? kDefaultShouldAutorotate } return visibleViewController?.shouldAutorotate ?? kDefaultShouldAutorotate } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if let presentedController = topViewController?.presentedViewController, presentedController.isBeingPresented { return presentedViewController?.supportedInterfaceOrientations ?? kDefaultSupportedInterfaceOrientations } if let presentedController = topViewController?.presentedViewController, presentedController.isBeingDismissed { return topViewController?.supportedInterfaceOrientations ?? kDefaultSupportedInterfaceOrientations } return visibleViewController?.supportedInterfaceOrientations ?? kDefaultSupportedInterfaceOrientations } override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { if let presentedController = topViewController?.presentedViewController, presentedController.isBeingPresented { return presentedViewController?.preferredInterfaceOrientationForPresentation ?? kDefaultPreferredInterfaceOrientationForPresentation } if let presentedController = topViewController?.presentedViewController, presentedController.isBeingDismissed { return topViewController?.preferredInterfaceOrientationForPresentation ?? kDefaultPreferredInterfaceOrientationForPresentation } return visibleViewController?.preferredInterfaceOrientationForPresentation ?? kDefaultPreferredInterfaceOrientationForPresentation } override var prefersStatusBarHidden: Bool { if let presentedController = topViewController?.presentedViewController, presentedController.isBeingPresented { return presentedViewController?.prefersStatusBarHidden ?? kDefaultPrefersStatusBarHidden } if let presentedController = topViewController?.presentedViewController, presentedController.isBeingDismissed { return topViewController?.prefersStatusBarHidden ?? kDefaultPrefersStatusBarHidden } return visibleViewController?.prefersStatusBarHidden ?? kDefaultPrefersStatusBarHidden } override var preferredStatusBarStyle: UIStatusBarStyle { if let presentedController = topViewController?.presentedViewController, presentedController.isBeingPresented { return presentedViewController?.preferredStatusBarStyle ?? kDefaultPreferredStatusBarStyle } if let presentedController = topViewController?.presentedViewController, presentedController.isBeingDismissed { return topViewController?.preferredStatusBarStyle ?? kDefaultPreferredStatusBarStyle } return visibleViewController?.preferredStatusBarStyle ?? kDefaultPreferredStatusBarStyle } } extension BaseNavViewController: UIGestureRecognizerDelegate { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let controller = topViewController, controller.isForbidInteractivePopGesture { return false // 播放器處于橫屏時,禁用左滑手勢 } return viewControllers.count > 1 } }
這里這么多代碼,其實都是一個處理邏輯,原則如下:
如果你不了解導航控制器的
topViewController
、visibleViewController
、視圖控制器的presentedViewController
、presentingViewController
是什么概念,那么建議百度 or Google 一下再看下面的內容,這里就不做普及了,以免篇幅過長。- 判斷導航控制器棧頂的視圖控制器
topViewController
是否有presentedViewController
,如果有,并且正在被 present 當中,則優先使用該presentedViewController
的配置參數。 - 判斷導航控制器棧頂的視圖控制器
topViewController
是否有presentedViewController
,如果有,并且正在被 dismiss 當中,則優先使用該topViewController
的配置參數。 - 剩下的是默認配置,不再判斷有沒有
presentedViewController
,也不再判斷presentedViewController
的狀態,由系統決定。是使用presentedViewController
還是使用topViewController
。 - 左滑返回手勢是否開啟由兩個原則,一是如果視圖控制器返回的
isForbidInteractivePopGesture
為true
時禁用,二是 默認判斷 視圖控制器的堆棧中視圖控制器的數量,大于 1 時可用。
- 判斷導航控制器棧頂的視圖控制器
兩大容器類型的視圖控制器重寫完了,接下來我們來寫其他三個。
-
配置
BaseViewController
:class BaseViewController: UIViewController { // MARK: - 關于旋轉的一些配置和說明 // _xxx_ 系列方法,由子類自定義實現,未實現時,使用下面的默認參數 var _preferredStatusBarStyle_: UIStatusBarStyle? { return nil } var _prefersStatusBarHidden_: Bool? { return nil } var _shouldAutorotate_: Bool? { return nil } var _supportedInterfaceOrientations_: UIInterfaceOrientationMask? { return nil } var _preferredInterfaceOrientationForPresentation_: UIInterfaceOrientation? { return nil } override var preferredStatusBarStyle: UIStatusBarStyle { if let presentedController = presentedViewController, presentedController.isBeingPresented { return presentedController.preferredStatusBarStyle } if let presentedController = presentedViewController, presentedController.isBeingDismissed { return _preferredStatusBarStyle_ ?? kDefaultPreferredStatusBarStyle } if let presentedController = presentedViewController { return presentedController.preferredStatusBarStyle } return _preferredStatusBarStyle_ ?? kDefaultPreferredStatusBarStyle } override var prefersStatusBarHidden: Bool { if let presentedController = presentedViewController, presentedController.isBeingPresented { return presentedController.prefersStatusBarHidden } if let presentedController = presentedViewController, presentedController.isBeingDismissed { return _prefersStatusBarHidden_ ?? kDefaultPrefersStatusBarHidden } if let presentedController = presentedViewController { return presentedController.prefersStatusBarHidden } return _prefersStatusBarHidden_ ?? kDefaultPrefersStatusBarHidden } override var shouldAutorotate: Bool { if let presentedController = presentedViewController, presentedController.isBeingPresented { return presentedController.shouldAutorotate } if let presentedController = presentedViewController, presentedController.isBeingDismissed { return _shouldAutorotate_ ?? kDefaultShouldAutorotate } if let presentedController = presentedViewController { return presentedController.shouldAutorotate } return _shouldAutorotate_ ?? kDefaultShouldAutorotate } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if let presentedController = presentedViewController, presentedController.isBeingPresented { return presentedController.supportedInterfaceOrientations } if let presentedController = presentedViewController, presentedController.isBeingDismissed { return _supportedInterfaceOrientations_ ?? kDefaultSupportedInterfaceOrientations } if let presentedController = presentedViewController { return presentedController.supportedInterfaceOrientations } return _supportedInterfaceOrientations_ ?? kDefaultSupportedInterfaceOrientations } override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { if let presentedController = presentedViewController, presentedController.isBeingPresented { return presentedController.preferredInterfaceOrientationForPresentation } if let presentedController = presentedViewController, presentedController.isBeingDismissed { return _preferredInterfaceOrientationForPresentation_ ?? kDefaultPreferredInterfaceOrientationForPresentation } if let presentedController = presentedViewController { return presentedController.preferredInterfaceOrientationForPresentation } return _preferredInterfaceOrientationForPresentation_ ?? kDefaultPreferredInterfaceOrientationForPresentation } }
又是一堆代碼... 真的不想貼這么多,但是有些人就知道復制黏貼...怕大家漏寫又來一通問,一通罵,怎么不行呀!片紙!!!!片紙!!!! ...,下面還是說一下處理邏輯:
- 如果存在
presentedViewController
,并且正在被present
,則優先使用presentedViewController
的配置參數。 - 如果存在
presentedViewController
,并且正在被dismiss
,則優先使用當前控制器的參數配置,如果子類沒有重寫對應的系列_xxx_
方法,則使用默認參數。 - 如果存在
presentedViewController
(說明它當前正在被顯示),則優先使用presentedViewController
的配置參數。 - 最后,使用子類自定義(如果子類有重寫對應的系列
_xxx_
方法)或默認配置。
- 如果存在
-
配置
BaseTableViewController
:class BaseTableViewController: UITableViewControlelr { // 和 BaseViewController 中一模一樣的代碼,直接黏貼過來即可。 }
-
配置
BaseCollectionViewController
:class BaseTableViewController: UITableViewControlelr { // 和 BaseViewController 中一模一樣的代碼,直接黏貼過來即可。 }
-
五大基礎類重寫完畢,在介紹具體的使用場景之前,需要再寫一個類,拿來控制旋轉方向的,其實就是調用
UIDevice.current.setValue(UIInterfaceOrientation.xxx.rawValue: forKey:"orientation")
來設置方向的,因為這個方法涉及到了運行時
、kvc
等黑魔法概念,所以我做了一個包裝,其實最終的結果還是kvc
,只是不那么明顯而已,有點自娛自樂的 style ??,關于 私有API,孫源 大大這他的 這篇文章 中,說過他的理解,感興趣的朋友可以去看看。下面直接貼代碼:// MARK: - 專門負責旋轉屏的工具類 class UIRotateUtils { static let shared = UIRotateUtils() private var appOrientation: UIDevice { return UIDevice.current } /// 方向枚舉 enum Orientation { case portrait case portraitUpsideDown case landscapeRight case landscapeLeft case unknown var mapRawValue: Int { switch self { case .portrait: return UIInterfaceOrientation.portrait.rawValue case .portraitUpsideDown: return UIInterfaceOrientation.portraitUpsideDown.rawValue case .landscapeRight: return UIInterfaceOrientation.landscapeRight.rawValue case .landscapeLeft: return UIInterfaceOrientation.landscapeLeft.rawValue case .unknown: return UIInterfaceOrientation.unknown.rawValue } } } private let unicodes: [UInt8] = [ 111,// o -> 0 105,// i -> 1 101,// e -> 2 116,// t -> 3 114,// r -> 4 110,// n -> 5 97 // a -> 6 ] private lazy var key: String = { return [ self.unicodes[0],// o self.unicodes[4],// r self.unicodes[1],// i self.unicodes[2],// e self.unicodes[5],// n self.unicodes[3],// t self.unicodes[6],// a self.unicodes[3],// t self.unicodes[1],// i self.unicodes[0],// o self.unicodes[5] // n ].map { return String(Character(Unicode.Scalar ($0))) }.joined(separator: "") }() /// 旋轉到豎屏 /// /// - Parameter orientation: 方向枚舉 func rotateToPortrait(_ orientation: Orientation = .portrait) { rotate(to: orientation) } /// 旋轉到橫屏 /// /// - Parameter orientation: 方向枚舉 func rotateToLandscape(_ orientation: Orientation = .landscapeRight) { rotate(to: orientation) } /// 旋轉到指定方向 /// /// - Parameter orientation: 方向枚舉 func rotate(to orientation: Orientation) { appOrientation.setValue(Orientation.unknown.mapRawValue, forKey: key) // ?? 需要先設置成 unknown 喲 appOrientation.setValue(orientation.mapRawValue, forKey: key) } }
有一點需要注意的是,設置實際所需方向之前,需要先設置一次方向為
unknown
, 因為可能會出現意外情況,導致你設置指定方向時,當前的設備方向已經就是這個方向了,UIKit就不會觸發相關事件,并不會重繪界面,進而導致調用無效的情況。 -
播放器視圖控制器
PlayerViewController
:class PlayerViewController: BaseViewController { // 此參數由外部傳入,并且在要在構造控制器時傳入 fileprivate var _isLandscape = false init(isLandscape: Bool = false) { ... _isLandscape = isLandscape ... } override func viewDidLoad() { super.viewDidLoad() updateOrientationIfNeeded(true)// 剛啟動時,強制執行 } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateOrientationIfNeeded()// 后續的界面間跳轉,不強制執行 } // MARK: - 自定義配置 override var _prefersStatusBarHidden_: Bool? { return true } override var _supportedInterfaceOrientations_: UIInterfaceOrientationMask? { return _isLandscape ? .landscapeRight: .portrait } override var _preferredInterfaceOrientationForPresentation_: UIInterfaceOrientation? { return _isLandscape ? .landscapeRight: .portrait } override var isForbidInteractivePopGesture: Bool { return _isLandscape } // MARK: - 控制旋轉 fileprivate func updateOrientationIfNeeded(_ force: Bool = false) { if _isLandscape { toLandscapeOrientation(force) } else { toPortraitOrientation(force) } } fileprivate func toLandscapeOrientation(_ force: Bool = false) { guard force || !_isLandscape else { return } UIRotateUtils.shared.rotateToLandscape() } fileprivate func toPortraitOrientation(_ force: Bool = false) { guard force || _isLandscape else { return } UIRotateUtils.shared.rotateToPortrait() } // 點擊菜單的 “旋轉” 按鈕 @objc fileprivate func onChangeOrientationBtnTapped(_ any: Any?) { ... ... // 核心控制 _isLandscape = !_isLandscape if _isLandscape { toLandscapeOrientation(true) } else { toPortraitOrientation(true) } ... ... } }
播放器大概的配置就這些,也很簡單,主要的注意點在于:
- 控制好變量
_isLandscape
的傳入時機,一定要在視圖控制器進入之前傳入,建議是構造視圖控制器時就傳入。 -
viewDidLoad
和viewWillAppear
都執行updateOrientationIfNeeded
方法。 - 通過
_isLandscape
控制_supportedInterfaceOrientations_
和_preferredInterfaceOrientationForPresentation_
的返回值。
- 控制好變量
-
評論輸入框界面
WriteCommentViewController
:場景案例 中提到過,一般這種界面像是懸浮在上一個界面之上,存在半透明的界面部分,可以看到上一界面的視圖,而且,在不重寫轉場動畫的情況下,一般使用
present
的形式,以模態視圖的形式呈現。更多關于 轉場動畫 的相關知識,請看 唐巧 大大的 這篇文章 ,你一定會收益匪淺。class WriteCommentViewController: BaseViewController { // 此參數由外部傳入,并且在要在構造控制器時傳入 fileprivate var _isLandscape = false init(isLandscape: Bool = false) { ... _isLandscape = isLandscape modalPresentationStyle = .overFullScreen modalTransitionStyle = .crossDissolve ... } override var _supportedInterfaceOrientations_: UIInterfaceOrientationMask? { return _isLandscape ? .landscapeRight : .portrait } override var _preferredInterfaceOrientationForPresentation_: UIInterfaceOrientation? { return _isLandscape ? .landscapeRight : .portrait } override var _prefersStatusBarHidden_: Bool? { return true } }
基礎配置和
PlayerViewController
差不多,需要注意的一點是:- 因為界面是
present
出來的,并且不自定義轉場動畫時,需要配置modalPresentationStyle
和modalTransitionStyle
,轉場樣式可以自己指定,modalPresentationStyle
目前我沒有使用.custom
模式,使用overFullScreen
問題相對少一點。 - 如果你的界面中也存在需要半透明或者透明度的部分,則需要把視圖控制器的
view
的backgroundColor
設置成透明,然后自己加一層黑色背景的控件,用一個alpha
動畫漸變到小于1.0的某個值。
- 因為界面是
-
目錄
CategoryViewController
:class CategoryViewController: BaseViewController { // 此參數由外部傳入,并且在要在構造控制器時傳入 fileprivate var _isLandscape = false init(isLandscape: Bool = false) { ... _isLandscape = isLandscape ... } override var _supportedInterfaceOrientations_: UIInterfaceOrientationMask? { return _isLandscape ? .landscapeRight : .portrait } override var _preferredInterfaceOrientationForPresentation_: UIInterfaceOrientation? { return _isLandscape ? .landscapeRight : .portrait } }
基本和上面的兩個類的配置一致。
-
登錄
UserLoginViewController
:場景案例 中描述過,登錄 界面是被
present
出來的,并且還能push
到 注冊 界面,因此 登錄 界面是被包裹在 導航控制器 中的。class UserLoginViewController: BaseTableViewController { // 標識登錄界面被 present 打開時,上一個界面(播放器)是不是處于橫屏狀態 fileprivate var _isPreViewControllerAtLandscapeMode = false filepriate var _loginActionResultBlock: ((Bool) -> Void)? = nil // 外部調用方式: // presentingViewController.present(UserLoginViewController.viewController(_isLandscape, animated: true) // class func viewController(_ isPreViewControllerAtLandscapeMode: Bool = false, loginActionResultBlock: ((Bool) -> Void)? = nil, ...) -> BaseNavViewController { // 構建登錄視圖控制器的方式,自定,一般都是通過StoryBoard來布局。 let loginController = UIStoryboard(name: "Login", bundle: nil).instantiateViewController(withIdentifier: "Login_VC") as! UserLoginViewController loginController._isPreViewControllerAtLandscapeMode = isPreViewControllerAtLandscapeMode loginController._loginActionResultBlock = loginActionResultBlock ... ... // 包裝到BaseNavViewController中去 let nav = BaseNavViewController(rootViewController: loginController) nav.modalPresentationStyle = .fullScreen nav.modalTransitionStyle = .coverVertical return nav } ... ... override var _supportedInterfaceOrientations_: UIInterfaceOrientationMask? { return .portrait // 豎屏 } override var _preferredInterfaceOrientationForPresentation_: UIInterfaceOrientation? { return .portrait // 豎屏 } override var _preferredStatusBarStyle_: UIStatusBarStyle? { return .lightContent // 返回你自己需要的狀態欄樣式 } // 關閉登錄界面(不管在登錄界面中是否調到了別的界面,注意,一定是返回到登錄界面之后,再統一關閉,因為這里需要額外處理一下) fileprivate func closeController(_ isLoginSuccess: Bool) { // 關閉界面之前,處理一下旋轉問題 if _isPreViewControllerAtLandscapeMode { UIRotateUtils.shared.rotateToLandscape() } dismiss(animated: true) { [weak self] _ in self?._loginActionResultBlock?(isLoginSuccess) } } ... ... }
基本配置就這些,至于 注冊 界面想支持什么類型的方向,可以隨意定制。因為五個基礎類已經做了大部分的工作,如果想支持特定方向,就需要自己重寫幾個
_xxx_
系列方法來自定義了,默認只支持豎屏。需要注意的是包裝 登錄 界面的導航控制器的
modalPresentationStyle
和modalTransitionStyle
的配置。modalPresentationStyle
一定設置成.fullScreen
, 不過這個是系統默認設置,這里只是保險起見。modalTransitionStyle
一般情況下,登錄 界面都是以.coverVertical
的形式出現的。
最后
最后的最后,做一個簡單的總結。
- 五個跟旋轉屏,狀態欄樣式有關系的屬性,從根視圖控制器一路傳到最頂級視圖。分別是:
- prefersStatusBarHidden
- preferredStatusBarStyle
- shouldAutorotate
- supportedInterfaceOrientations
- preferredInterfaceOrientationForPresentation
- 確保返回的
supportedInterfaceOrientations
的相關值總類型 包含于preferredInterfaceOrientationForPresentation
返回的對應類型值。 - 處理好
UINavigationController
中的上述五個屬性,理清topViewController
visibleViewController
以及 被present
出來的模態視圖控制器的isBeingPresented
和isBeingDismissed
屬性的含義。 - 處理好 基礎視圖控制器 中的
presentedViewController
及 理清其對應的isBeingPresented
和isBeingDismissed
屬性的含義。 - 【一個很重要的點忘記提及了】鍵盤彈出的布局方向和視圖控制器返回的supportedInterfaceOrientation是一致的,與你的狀態欄方向無關。
Happy 2018. Happy New Year!
有問題請在簡書中發送私信或者關注我的個人 微博,給我留言。謝謝關注,如果您有更多的想法,請聯系互相交流。
Demo 在此,歡迎star!!!
TODO_List:
- 第三方APP調起時的相關配置稍后補上。