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

圖有點(diǎn)大,可以打開一個新標(biāo)簽放大查看,我們項(xiàng)目APP的一個大概的結(jié)構(gòu)圖,主要指示了一下涉及到旋轉(zhuǎn)屏的視圖控制器,以及各個控制器之間的關(guān)系,是Push出來的還是Present出來的。
簡單描述一下場景:
- 主視圖控制器是一個繼承自 UITabBarController 的視圖控制器。
- 底部有四個Tab,四個Tab分別指向繼承自 UINavigationController 的視圖控制器作為根視圖。
- 通常情況下,都是豎屏,四個Tab的部分界面中都有跳播放器視圖控制器的入口。
- 進(jìn)播放器時(shí),有兩種方式進(jìn)入,豎屏 or 橫屏。
- 第一次是默認(rèn)豎屏,之后進(jìn)入時(shí),由用戶最后退出播放器時(shí)的閱讀方向來決定。
- 播放器中有四個菜單和一個評論輸入框。
- 點(diǎn)擊 評論輸入框,彈出一個可輸入評論的視圖控制器,以 present 的形式彈出,會覆蓋在播放器之上,并且能看到后面的播放器內(nèi)容。方向與當(dāng)前閱讀器的方向一致。
- 點(diǎn)擊 目錄,以 push 的方式打開目錄頁。目錄頁方向與播放器方向一致。(之前的需求是目錄頁要以豎屏的方式出現(xiàn),當(dāng)然,這個也可以實(shí)現(xiàn),下面會說解決方案)
- 點(diǎn)擊 旋轉(zhuǎn) 菜單,切換播放器方向,豎屏 -> 橫屏,or 橫屏 -> 豎屏。
- 用戶在輸入評論之后,點(diǎn)擊右邊或者鍵盤的的 發(fā)送 按鈕,會先判斷當(dāng)前用戶的登錄狀態(tài),如果未登錄或者登錄信息失效,會 present 一個 豎屏 的 登錄界面。
- 登錄界面 同樣包裝在一個 UINavigationController 之中,用戶未注冊時(shí)還可以 push 到一個 注冊 界面,同樣也是豎屏,第三方登錄方式有 微信,QQ,微博 等。
- 播放器可以被外部APP調(diào)起,諸如 Safari瀏覽器 或者 QQ瀏覽器。(為什么要說到這一點(diǎn),是因?yàn)楫?dāng)用在在這些外部APP中調(diào)起播放器時(shí),用戶手持手機(jī)的方向會直接影響到調(diào)起之后,播放器的方向,處理不好的話就會錯亂,比如之前播放器時(shí)橫屏,從外部APP調(diào)起時(shí),手機(jī)又是豎屏。)
了解一點(diǎn)基礎(chǔ)知識
在講解我的處理方案之前,我想先跟大家介紹一下Apple的官方文檔關(guān)于旋轉(zhuǎn)屏?xí)r的處理機(jī)制。
在Apple Documentation 中 關(guān)于 UIViewController 的介紹中,簡要提到過旋轉(zhuǎn)屏?xí)r,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開始,所有旋轉(zhuǎn)相關(guān)的方法都被廢棄。旋轉(zhuǎn)被視為是視圖控制器的view的大小的改變并在viewWillTransition(to:with:) 方法中反饋給視圖控制器。當(dāng)界面方向發(fā)生改變,UIKit會在窗口的根視圖控制器中調(diào)用此方法,然后根視圖控制器再通知它所管理的其他子視圖控制器。此消息將在整個視圖控制器棧中傳播貫穿。
In iOS 6 and iOS 7, your app supports the interface orientations defined in your app’s Info.plist file.
在iOS6和iOS7中,你的程序所支持的界面方向由程序的info.plist文件中定義的參數(shù)決定。
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 來控制支持的方向。通常情況下,系統(tǒng)只在window的rootViewController和一個充滿全屏的模態(tài)(presented view controller)視圖中調(diào)用此方法。
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.
子視圖不直接參與旋轉(zhuǎn)方向的決策,直接由它們的父視圖決定。
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.
程序支持的方向和視圖控制器支持的方向的交集被用來決定視圖控制器應(yīng)該旋轉(zhuǎn)到哪個方向。
You can override the preferredInterfaceOrientationForPresentation for a view controller that is intended to be presented full screen in a specific orientation.
你可以為一個準(zhǔn)備present成一個全屏的模態(tài)視圖控制器通過重寫 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.
對于一個可見的視圖控制器,當(dāng)旋轉(zhuǎn)發(fā)生時(shí),這些方法willRotate(to:duration:), willAnimateRotation(to:duration:), 和 didRotate(from:) 會在旋轉(zhuǎn)過程中被調(diào)用,當(dāng)視圖控制器的view被重新拉伸并被父視圖定位完成時(shí),viewWillLayoutSubviews() 將被調(diào)用。如果一個視圖控制器在旋轉(zhuǎn)過程中處于不可見狀態(tài),那么上面提到的三個方法不會被調(diào)用。然而,在視圖重新可見時(shí),viewWillLayoutSubviews() 會被調(diào)用。你可以重寫此方法并在該方法中調(diào)用 statusBarOrientation 方法來決定設(shè)備的方向。
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.
注意
在程序應(yīng)該在啟動時(shí)保持豎屏,等到application(_:didFinishLaunchingWithOptions:) 方法返回之后,程序再使用上面提到過的旋轉(zhuǎn)機(jī)制來合理的處理窗口視圖的旋轉(zhuǎn)。
額外說一下 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獲取和設(shè)置,還有另外一個方法來設(shè)置這個屬性的值,可以傳遞動畫與否的參數(shù),UIApplication.shared.setStatusBarOrientation(:, animated: ),直接設(shè)置這個屬性值,相當(dāng)于調(diào)用了該方法時(shí)傳入了animated: false,即不使用任何動畫形式來改變狀態(tài)欄的方向。- 如果你的程序中的某個視圖控制器的界面是可旋轉(zhuǎn)的,那么你不應(yīng)該隨意的去設(shè)置這個屬性,意圖改變狀態(tài)欄的方向,因?yàn)檫@將可能無效。(我就曾遇到過,邏輯都是從另外一個項(xiàng)目中照搬過來的,但是調(diào)用此方法時(shí),死活不改變方向。當(dāng)然,這跟你是否正確的返回
shouldAutorotate有關(guān)系,下面會講到。)- 作為總結(jié),如果你的當(dāng)前視圖控制器的
shouldAutorotate返回true,則盡量不要再去調(diào)用UIApplication.shared.statusBarOrientation了, 一是可能無效,二是statusBarOrientation的方向會隨著你返回的supportedInterfaceOrientation改變而自動改變。
正題
按照官方的說法,我打算一步一步的告訴大家,如何配置,如何編寫代碼,從最根部,到最外層。
-
首先,配置程序的info.plist配置文件,只勾選豎屏,這樣可以保證豎屏啟動界面 (即 LaunchScreen.storyboard 配置的程序默認(rèn)啟動界面在任何情況下都豎屏啟動)。
程序Info.plist的配置 -
在
AppDelegate中的配置:@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { ... func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return .allButUpsideDown } ... }- 當(dāng)然,如果你的程序支持 iPad ,可以返回
.all來支持所有的方向。 - 一般情況下,返回
.allButUpsideDown就夠了。 - 前面講到過,
UIKit會取視圖控制器返回的值和當(dāng)前返回的值,做一個交叉,取交叉值,所有這里返回最大范圍的支持方向。
- 當(dāng)然,如果你的程序支持 iPad ,可以返回
-
自定義五個基類,分別是:
-
BaseTabBarController,繼承自UITabBarControlelr -
BaseNavViewController,繼承自UINavigationController -
BaseViewController,繼承自UIViewController -
BaseTableViewController,繼承自UITableViewController -
BaseCollectionViewController,繼承自UICollectionViewController
這五個基類基本上覆蓋了程序的大部分需要的視圖控制器,如果您的程序中還有其他類型的視圖控制器,照著下面我所描述的原理,配置一下即可。
-
先寫上一個 swift 文件,為程序配置幾個默認(rèn)配置的屬性,供全局使用,并配置一些相關(guān)拓展,下面會用到。
// 基礎(chǔ)視圖控制器的默認(rèn)配置,涵蓋了跟旋轉(zhuǎn)屏、present時(shí)屏幕方向和狀態(tài)欄樣式有關(guān)系的常用配置 let kDefaultPreferredStatusBarStyle: UIStatusBarStyle = .default // 狀態(tài)欄樣式,默認(rèn)使用系統(tǒng)的 let kDefaultPrefersStatusBarHidden: Bool = false // 狀態(tài)欄是否隱藏,默認(rèn)不隱藏 let kDefaultShouldAutorotate: Bool = true // 是否支持屏幕旋轉(zhuǎn),默認(rèn)支持 let kDefaultSupportedInterfaceOrientations: UIInterfaceOrientationMask = .portrait // 支持的旋轉(zhuǎn)方向,默認(rèn)豎屏 let kDefaultPreferredInterfaceOrientationForPresentation: UIInterfaceOrientation = .portrait // present時(shí),打開視圖控制器的方向,默認(rèn)豎屏 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 { // 是否禁用導(dǎo)航欄的左滑手勢,默認(rèn)不禁用 var isForbidInteractivePopGesture: Bool { return false } }
額呵,只有這么一個簡單的配置,為的是在播放器處于橫屏?xí)r,禁用導(dǎo)航控制器的左滑返回手勢,豎屏?xí)r正常可用。
為什么要禁用?。?!
因?yàn)樯弦粋€界面是豎屏??!而播放器也是被 Push 進(jìn)來的。so!要么禁用,要么一觸發(fā)滑動,界面就立刻關(guān)閉了,體驗(yàn)不好。
-
配置
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 作為根視圖,需要把參數(shù)傳遞給它的子視圖。
注意:上面的代碼,重寫
supportedInterfaceOrientations時(shí),也取了preferredInterfaceOrientationForPresentation的值并做了一個轉(zhuǎn)換,之所以這么處理,是因?yàn)楹芏嗲闆r下,我們會無意間返回與supportedInterfaceOrientations不一致的方向,導(dǎo)致這種錯誤:UIApplicationInvalidInterfaceOrientation: preferredInterfaceOrientationForPresentation 'landscapeRight' must match a supported interface orientation: 'portrait'!
可以看出,系統(tǒng)要求我們返回的
supportedInterfaceOrientations與preferredInterfaceOrientationForPresentation至少要有可交叉的值,UIInterfaceOrientation只能定義一個值,UIInterfaceOrientationMask支持OptionSet協(xié)議 可返回一個數(shù)組,因此可以是多個值,所以可做如上處理,避免你沒有重寫preferredInterfaceOrientationForPresentation由系統(tǒng)返回的默認(rèn)值 或者 你重寫了,但是由于代碼邏輯錯誤,返回了一個與supportedInterfaceOrientations方向不一致的值。
-
配置
BaseNavViewController:class BaseNavViewController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() interactivePopGestureRecognizer?.delegate = self // 切記不要放在構(gòu)造方法中配置,因?yàn)槟菚r(shí)的 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 // 播放器處于橫屏?xí)r,禁用左滑手勢 } return viewControllers.count > 1 } }這里這么多代碼,其實(shí)都是一個處理邏輯,原則如下:
如果你不了解導(dǎo)航控制器的
topViewController、visibleViewController、視圖控制器的presentedViewController、presentingViewController是什么概念,那么建議百度 or Google 一下再看下面的內(nèi)容,這里就不做普及了,以免篇幅過長。- 判斷導(dǎo)航控制器棧頂?shù)囊晥D控制器
topViewController是否有presentedViewController,如果有,并且正在被 present 當(dāng)中,則優(yōu)先使用該presentedViewController的配置參數(shù)。 - 判斷導(dǎo)航控制器棧頂?shù)囊晥D控制器
topViewController是否有presentedViewController,如果有,并且正在被 dismiss 當(dāng)中,則優(yōu)先使用該topViewController的配置參數(shù)。 - 剩下的是默認(rèn)配置,不再判斷有沒有
presentedViewController,也不再判斷presentedViewController的狀態(tài),由系統(tǒng)決定。是使用presentedViewController還是使用topViewController。 - 左滑返回手勢是否開啟由兩個原則,一是如果視圖控制器返回的
isForbidInteractivePopGesture為true時(shí)禁用,二是 默認(rèn)判斷 視圖控制器的堆棧中視圖控制器的數(shù)量,大于 1 時(shí)可用。
- 判斷導(dǎo)航控制器棧頂?shù)囊晥D控制器
兩大容器類型的視圖控制器重寫完了,接下來我們來寫其他三個。
-
配置
BaseViewController:class BaseViewController: UIViewController { // MARK: - 關(guān)于旋轉(zhuǎn)的一些配置和說明 // _xxx_ 系列方法,由子類自定義實(shí)現(xiàn),未實(shí)現(xiàn)時(shí),使用下面的默認(rèn)參數(shù) 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 } }又是一堆代碼... 真的不想貼這么多,但是有些人就知道復(fù)制黏貼...怕大家漏寫又來一通問,一通罵,怎么不行呀!片紙!!!!片紙!!!! ...,下面還是說一下處理邏輯:
- 如果存在
presentedViewController,并且正在被present,則優(yōu)先使用presentedViewController的配置參數(shù)。 - 如果存在
presentedViewController,并且正在被dismiss,則優(yōu)先使用當(dāng)前控制器的參數(shù)配置,如果子類沒有重寫對應(yīng)的系列_xxx_方法,則使用默認(rèn)參數(shù)。 - 如果存在
presentedViewController(說明它當(dāng)前正在被顯示),則優(yōu)先使用presentedViewController的配置參數(shù)。 - 最后,使用子類自定義(如果子類有重寫對應(yīng)的系列
_xxx_方法)或默認(rèn)配置。
- 如果存在
-
配置
BaseTableViewController:class BaseTableViewController: UITableViewControlelr { // 和 BaseViewController 中一模一樣的代碼,直接黏貼過來即可。 } -
配置
BaseCollectionViewController:class BaseTableViewController: UITableViewControlelr { // 和 BaseViewController 中一模一樣的代碼,直接黏貼過來即可。 } -
五大基礎(chǔ)類重寫完畢,在介紹具體的使用場景之前,需要再寫一個類,拿來控制旋轉(zhuǎn)方向的,其實(shí)就是調(diào)用
UIDevice.current.setValue(UIInterfaceOrientation.xxx.rawValue: forKey:"orientation")來設(shè)置方向的,因?yàn)檫@個方法涉及到了運(yùn)行時(shí)、kvc等黑魔法概念,所以我做了一個包裝,其實(shí)最終的結(jié)果還是kvc,只是不那么明顯而已,有點(diǎn)自娛自樂的 style ??,關(guān)于 私有API,孫源 大大這他的 這篇文章 中,說過他的理解,感興趣的朋友可以去看看。下面直接貼代碼:// MARK: - 專門負(fù)責(zé)旋轉(zhuǎn)屏的工具類 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: "") }() /// 旋轉(zhuǎn)到豎屏 /// /// - Parameter orientation: 方向枚舉 func rotateToPortrait(_ orientation: Orientation = .portrait) { rotate(to: orientation) } /// 旋轉(zhuǎn)到橫屏 /// /// - Parameter orientation: 方向枚舉 func rotateToLandscape(_ orientation: Orientation = .landscapeRight) { rotate(to: orientation) } /// 旋轉(zhuǎn)到指定方向 /// /// - Parameter orientation: 方向枚舉 func rotate(to orientation: Orientation) { appOrientation.setValue(Orientation.unknown.mapRawValue, forKey: key) // ?? 需要先設(shè)置成 unknown 喲 appOrientation.setValue(orientation.mapRawValue, forKey: key) } }有一點(diǎn)需要注意的是,設(shè)置實(shí)際所需方向之前,需要先設(shè)置一次方向?yàn)?
unknown, 因?yàn)榭赡軙霈F(xiàn)意外情況,導(dǎo)致你設(shè)置指定方向時(shí),當(dāng)前的設(shè)備方向已經(jīng)就是這個方向了,UIKit就不會觸發(fā)相關(guān)事件,并不會重繪界面,進(jìn)而導(dǎo)致調(diào)用無效的情況。 -
播放器視圖控制器
PlayerViewController:class PlayerViewController: BaseViewController { // 此參數(shù)由外部傳入,并且在要在構(gòu)造控制器時(shí)傳入 fileprivate var _isLandscape = false init(isLandscape: Bool = false) { ... _isLandscape = isLandscape ... } override func viewDidLoad() { super.viewDidLoad() updateOrientationIfNeeded(true)// 剛啟動時(shí),強(qiáng)制執(zhí)行 } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateOrientationIfNeeded()// 后續(xù)的界面間跳轉(zhuǎn),不強(qiáng)制執(zhí)行 } // 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: - 控制旋轉(zhuǎn) 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() } // 點(diǎn)擊菜單的 “旋轉(zhuǎn)” 按鈕 @objc fileprivate func onChangeOrientationBtnTapped(_ any: Any?) { ... ... // 核心控制 _isLandscape = !_isLandscape if _isLandscape { toLandscapeOrientation(true) } else { toPortraitOrientation(true) } ... ... } }播放器大概的配置就這些,也很簡單,主要的注意點(diǎn)在于:
- 控制好變量
_isLandscape的傳入時(shí)機(jī),一定要在視圖控制器進(jìn)入之前傳入,建議是構(gòu)造視圖控制器時(shí)就傳入。 -
viewDidLoad和viewWillAppear都執(zhí)行updateOrientationIfNeeded方法。 - 通過
_isLandscape控制_supportedInterfaceOrientations_和_preferredInterfaceOrientationForPresentation_的返回值。
- 控制好變量
-
評論輸入框界面
WriteCommentViewController:場景案例 中提到過,一般這種界面像是懸浮在上一個界面之上,存在半透明的界面部分,可以看到上一界面的視圖,而且,在不重寫轉(zhuǎn)場動畫的情況下,一般使用
present的形式,以模態(tài)視圖的形式呈現(xiàn)。更多關(guān)于 轉(zhuǎn)場動畫 的相關(guān)知識,請看 唐巧 大大的 這篇文章 ,你一定會收益匪淺。class WriteCommentViewController: BaseViewController { // 此參數(shù)由外部傳入,并且在要在構(gòu)造控制器時(shí)傳入 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 } }基礎(chǔ)配置和
PlayerViewController差不多,需要注意的一點(diǎn)是:- 因?yàn)榻缑媸?
present出來的,并且不自定義轉(zhuǎn)場動畫時(shí),需要配置modalPresentationStyle和modalTransitionStyle,轉(zhuǎn)場樣式可以自己指定,modalPresentationStyle目前我沒有使用.custom模式,使用overFullScreen問題相對少一點(diǎn)。 - 如果你的界面中也存在需要半透明或者透明度的部分,則需要把視圖控制器的
view的backgroundColor設(shè)置成透明,然后自己加一層黑色背景的控件,用一個alpha動畫漸變到小于1.0的某個值。
- 因?yàn)榻缑媸?
-
目錄
CategoryViewController:class CategoryViewController: BaseViewController { // 此參數(shù)由外部傳入,并且在要在構(gòu)造控制器時(shí)傳入 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到 注冊 界面,因此 登錄 界面是被包裹在 導(dǎo)航控制器 中的。class UserLoginViewController: BaseTableViewController { // 標(biāo)識登錄界面被 present 打開時(shí),上一個界面(播放器)是不是處于橫屏狀態(tài) fileprivate var _isPreViewControllerAtLandscapeMode = false filepriate var _loginActionResultBlock: ((Bool) -> Void)? = nil // 外部調(diào)用方式: // presentingViewController.present(UserLoginViewController.viewController(_isLandscape, animated: true) // class func viewController(_ isPreViewControllerAtLandscapeMode: Bool = false, loginActionResultBlock: ((Bool) -> Void)? = nil, ...) -> BaseNavViewController { // 構(gòu)建登錄視圖控制器的方式,自定,一般都是通過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 // 返回你自己需要的狀態(tài)欄樣式 } // 關(guān)閉登錄界面(不管在登錄界面中是否調(diào)到了別的界面,注意,一定是返回到登錄界面之后,再統(tǒng)一關(guān)閉,因?yàn)檫@里需要額外處理一下) fileprivate func closeController(_ isLoginSuccess: Bool) { // 關(guān)閉界面之前,處理一下旋轉(zhuǎn)問題 if _isPreViewControllerAtLandscapeMode { UIRotateUtils.shared.rotateToLandscape() } dismiss(animated: true) { [weak self] _ in self?._loginActionResultBlock?(isLoginSuccess) } } ... ... }基本配置就這些,至于 注冊 界面想支持什么類型的方向,可以隨意定制。因?yàn)槲鍌€基礎(chǔ)類已經(jīng)做了大部分的工作,如果想支持特定方向,就需要自己重寫幾個
_xxx_系列方法來自定義了,默認(rèn)只支持豎屏。需要注意的是包裝 登錄 界面的導(dǎo)航控制器的
modalPresentationStyle和modalTransitionStyle的配置。modalPresentationStyle一定設(shè)置成.fullScreen, 不過這個是系統(tǒng)默認(rèn)設(shè)置,這里只是保險(xiǎn)起見。modalTransitionStyle一般情況下,登錄 界面都是以.coverVertical的形式出現(xiàn)的。
最后
最后的最后,做一個簡單的總結(jié)。
- 五個跟旋轉(zhuǎn)屏,狀態(tài)欄樣式有關(guān)系的屬性,從根視圖控制器一路傳到最頂級視圖。分別是:
- prefersStatusBarHidden
- preferredStatusBarStyle
- shouldAutorotate
- supportedInterfaceOrientations
- preferredInterfaceOrientationForPresentation
- 確保返回的
supportedInterfaceOrientations的相關(guān)值總類型 包含于preferredInterfaceOrientationForPresentation返回的對應(yīng)類型值。 - 處理好
UINavigationController中的上述五個屬性,理清topViewControllervisibleViewController以及 被present出來的模態(tài)視圖控制器的isBeingPresented和isBeingDismissed屬性的含義。 - 處理好 基礎(chǔ)視圖控制器 中的
presentedViewController及 理清其對應(yīng)的isBeingPresented和isBeingDismissed屬性的含義。 - 【一個很重要的點(diǎn)忘記提及了】鍵盤彈出的布局方向和視圖控制器返回的supportedInterfaceOrientation是一致的,與你的狀態(tài)欄方向無關(guān)。
Happy 2018. Happy New Year!
有問題請?jiān)诤啎邪l(fā)送私信或者關(guān)注我的個人 微博,給我留言。謝謝關(guān)注,如果您有更多的想法,請聯(lián)系互相交流。
Demo 在此,歡迎star!!!
TODO_List:
- 第三方APP調(diào)起時(shí)的相關(guān)配置稍后補(bǔ)上。
