本文基于 Swift 3.x瑟枫,由于 Swift 4.x 在語法規(guī)則上有較大變動,后續(xù)出一個 Swift 4.x 版本, Demo 工程在最下面砸西。
前言
我相信iOS的屏幕旋轉(zhuǎn)問題一直困擾著大多數(shù)的APP開發(fā)者叶眉,遇到界面需要旋轉(zhuǎn)址儒,特別是界面之間的關(guān)聯(lián)性很強,幾個視圖控制器又是Push又是Present衅疙,然后又交叉Push莲趣、Present...說到這里,腦海里就浮現(xiàn)出未找到解決方案時饱溢,想拍案而起抓狂的場景喧伞。
案例場景
圖有點大,可以打開一個新標(biāo)簽放大查看绩郎,我們項目APP的一個大概的結(jié)構(gòu)圖潘鲫,主要指示了一下涉及到旋轉(zhuǎn)屏的視圖控制器,以及各個控制器之間的關(guān)系肋杖,是Push出來的還是Present出來的溉仑。
簡單描述一下場景:
- 主視圖控制器是一個繼承自 UITabBarController 的視圖控制器。
- 底部有四個Tab状植,四個Tab分別指向繼承自 UINavigationController 的視圖控制器作為根視圖浊竟。
- 通常情況下,都是豎屏津畸,四個Tab的部分界面中都有跳播放器視圖控制器的入口振定。
- 進播放器時,有兩種方式進入肉拓,豎屏 or 橫屏后频。
- 第一次是默認(rèn)豎屏,之后進入時暖途,由用戶最后退出播放器時的閱讀方向來決定徘郭。
- 播放器中有四個菜單和一個評論輸入框。
- 點擊 評論輸入框丧肴,彈出一個可輸入評論的視圖控制器残揉,以 present 的形式彈出,會覆蓋在播放器之上芋浮,并且能看到后面的播放器內(nèi)容抱环。方向與當(dāng)前閱讀器的方向一致。
- 點擊 目錄纸巷,以 push 的方式打開目錄頁镇草。目錄頁方向與播放器方向一致。(之前的需求是目錄頁要以豎屏的方式出現(xiàn)瘤旨,當(dāng)然梯啤,這個也可以實現(xiàn),下面會說解決方案)
- 點擊 旋轉(zhuǎn) 菜單存哲,切換播放器方向因宇,豎屏 -> 橫屏七婴,or 橫屏 -> 豎屏。
- 用戶在輸入評論之后察滑,點擊右邊或者鍵盤的的 發(fā)送 按鈕打厘,會先判斷當(dāng)前用戶的登錄狀態(tài),如果未登錄或者登錄信息失效贺辰,會 present 一個 豎屏 的 登錄界面户盯。
- 登錄界面 同樣包裝在一個 UINavigationController 之中,用戶未注冊時還可以 push 到一個 注冊 界面饲化,同樣也是豎屏莽鸭,第三方登錄方式有 微信,QQ吃靠,微博 等硫眨。
- 播放器可以被外部APP調(diào)起,諸如 Safari瀏覽器 或者 QQ瀏覽器撩笆。(為什么要說到這一點捺球,是因為當(dāng)用在在這些外部APP中調(diào)起播放器時缸浦,用戶手持手機的方向會直接影響到調(diào)起之后夕冲,播放器的方向,處理不好的話就會錯亂裂逐,比如之前播放器時橫屏歹鱼,從外部APP調(diào)起時,手機又是豎屏卜高。)
了解一點基礎(chǔ)知識
在講解我的處理方案之前弥姻,我想先跟大家介紹一下Apple的官方文檔關(guān)于旋轉(zhuǎn)屏?xí)r的處理機制。
在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ā)生時,這些方法willRotate(to:duration:), willAnimateRotation(to:duration:), 和 didRotate(from:) 會在旋轉(zhuǎn)過程中被調(diào)用盾致,當(dāng)視圖控制器的view被重新拉伸并被父視圖定位完成時主经,viewWillLayoutSubviews() 將被調(diào)用。如果一個視圖控制器在旋轉(zhuǎn)過程中處于不可見狀態(tài)庭惜,那么上面提到的三個方法不會被調(diào)用罩驻。然而,在視圖重新可見時护赊,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)該在啟動時保持豎屏骏啰,等到application(_:didFinishLaunchingWithOptions:) 方法返回之后节吮,程序再使用上面提到過的旋轉(zhuǎn)機制來合理的處理窗口視圖的旋轉(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)用了該方法時傳入了animated: false
,即不使用任何動畫形式來改變狀態(tài)欄的方向帚豪。- 如果你的程序中的某個視圖控制器的界面是可旋轉(zhuǎn)的,那么你不應(yīng)該隨意的去設(shè)置這個屬性草丧,意圖改變狀態(tài)欄的方向狸臣,因為這將可能無效。(我就曾遇到過昌执,邏輯都是從另外一個項目中照搬過來的烛亦,但是調(diào)用此方法時,死活不改變方向仙蚜。當(dāng)然此洲,這跟你是否正確的返回
shouldAutorotate
有關(guān)系,下面會講到委粉。)- 作為總結(jié)呜师,如果你的當(dāng)前視圖控制器的
shouldAutorotate
返回true
,則盡量不要再去調(diào)用UIApplication.shared.statusBarOrientation
了, 一是可能無效,二是statusBarOrientation
的方向會隨著你返回的supportedInterfaceOrientation
改變而自動改變贾节。
正題
按照官方的說法汁汗,我打算一步一步的告訴大家衷畦,如何配置,如何編寫代碼知牌,從最根部祈争,到最外層。
-
首先角寸,配置程序的info.plist配置文件菩混,只勾選豎屏,這樣可以保證豎屏啟動界面 (即 LaunchScreen.storyboard 配置的程序默認(rèn)啟動界面在任何情況下都豎屏啟動)扁藕。
-
在
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時屏幕方向和狀態(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時莺禁,打開視圖控制器的方向,默認(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正澈苍郑可用缕粹。
為什么要禁用!V交础致开!
因為上一個界面是豎屏!萎馅!而播放器也是被 Push 進來的双戳。so!要么禁用糜芳,要么一觸發(fā)滑動飒货,界面就立刻關(guā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
時扣墩,也取了preferredInterfaceOrientationForPresentation
的值并做了一個轉(zhuǎn)換,之所以這么處理扛吞,是因為很多情況下呻惕,我們會無意間返回與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)造方法中配置村视,因為那時的 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 } }
這里這么多代碼,其實都是一個處理邏輯,原則如下:
如果你不了解導(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
時禁用,二是 默認(rèn)判斷 視圖控制器的堆棧中視圖控制器的數(shù)量暴构,大于 1 時可用跪呈。
- 判斷導(dǎo)航控制器棧頂?shù)囊晥D控制器
兩大容器類型的視圖控制器重寫完了,接下來我們來寫其他三個取逾。
-
配置
BaseViewController
:class BaseViewController: UIViewController { // MARK: - 關(guān)于旋轉(zhuǎn)的一些配置和說明 // _xxx_ 系列方法耗绿,由子類自定義實現(xiàn),未實現(xiàn)時菌赖,使用下面的默認(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)方向的强窖,其實就是調(diào)用
UIDevice.current.setValue(UIInterfaceOrientation.xxx.rawValue: forKey:"orientation")
來設(shè)置方向的削祈,因為這個方法涉及到了運行時
脑漫、kvc
等黑魔法概念咙崎,所以我做了一個包裝褪猛,其實最終的結(jié)果還是kvc
,只是不那么明顯而已跛璧,有點自娛自樂的 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) } }
有一點需要注意的是,設(shè)置實際所需方向之前荠察,需要先設(shè)置一次方向為
unknown
, 因為可能會出現(xiàn)意外情況置蜀,導(dǎo)致你設(shè)置指定方向時悉盆,當(dāng)前的設(shè)備方向已經(jīng)就是這個方向了盯荤,UIKit就不會觸發(fā)相關(guān)事件秋秤,并不會重繪界面灼卢,進而導(dǎo)致調(diào)用無效的情況堰怨。 -
播放器視圖控制器
PlayerViewController
:class PlayerViewController: BaseViewController { // 此參數(shù)由外部傳入备图,并且在要在構(gòu)造控制器時傳入 fileprivate var _isLandscape = false init(isLandscape: Bool = false) { ... _isLandscape = isLandscape ... } override func viewDidLoad() { super.viewDidLoad() updateOrientationIfNeeded(true)// 剛啟動時,強制執(zhí)行 } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateOrientationIfNeeded()// 后續(xù)的界面間跳轉(zhuǎn),不強制執(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() } // 點擊菜單的 “旋轉(zhuǎn)” 按鈕 @objc fileprivate func onChangeOrientationBtnTapped(_ any: Any?) { ... ... // 核心控制 _isLandscape = !_isLandscape if _isLandscape { toLandscapeOrientation(true) } else { toPortraitOrientation(true) } ... ... } }
播放器大概的配置就這些,也很簡單,主要的注意點在于:
- 控制好變量
_isLandscape
的傳入時機零院,一定要在視圖控制器進入之前傳入,建議是構(gòu)造視圖控制器時就傳入告抄。 -
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)造控制器時傳入 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
差不多姜胖,需要注意的一點是:- 因為界面是
present
出來的,并且不自定義轉(zhuǎn)場動畫時隧出,需要配置modalPresentationStyle
和modalTransitionStyle
,轉(zhuǎn)場樣式可以自己指定饲鄙,modalPresentationStyle
目前我沒有使用.custom
模式,使用overFullScreen
問題相對少一點。 - 如果你的界面中也存在需要半透明或者透明度的部分轴咱,則需要把視圖控制器的
view
的backgroundColor
設(shè)置成透明,然后自己加一層黑色背景的控件窖剑,用一個alpha
動畫漸變到小于1.0的某個值戈稿。
- 因為界面是
-
目錄
CategoryViewController
:class CategoryViewController: BaseViewController { // 此參數(shù)由外部傳入西土,并且在要在構(gòu)造控制器時傳入 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 打開時援所,上一個界面(播放器)是不是處于橫屏狀態(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)閉,因為這里需要額外處理一下) fileprivate func closeController(_ isLoginSuccess: Bool) { // 關(guān)閉界面之前挽牢,處理一下旋轉(zhuǎn)問題 if _isPreViewControllerAtLandscapeMode { UIRotateUtils.shared.rotateToLandscape() } dismiss(animated: true) { [weak self] _ in self?._loginActionResultBlock?(isLoginSuccess) } } ... ... }
基本配置就這些谱煤,至于 注冊 界面想支持什么類型的方向,可以隨意定制禽拔。因為五個基礎(chǔ)類已經(jīng)做了大部分的工作刘离,如果想支持特定方向,就需要自己重寫幾個
_xxx_
系列方法來自定義了睹栖,默認(rèn)只支持豎屏硫惕。需要注意的是包裝 登錄 界面的導(dǎo)航控制器的
modalPresentationStyle
和modalTransitionStyle
的配置。modalPresentationStyle
一定設(shè)置成.fullScreen
, 不過這個是系統(tǒng)默認(rèn)設(shè)置野来,這里只是保險起見恼除。modalTransitionStyle
一般情況下,登錄 界面都是以.coverVertical
的形式出現(xiàn)的曼氛。
最后
最后的最后豁辉,做一個簡單的總結(jié)。
- 五個跟旋轉(zhuǎn)屏舀患,狀態(tài)欄樣式有關(guān)系的屬性徽级,從根視圖控制器一路傳到最頂級視圖。分別是:
- prefersStatusBarHidden
- preferredStatusBarStyle
- shouldAutorotate
- supportedInterfaceOrientations
- preferredInterfaceOrientationForPresentation
- 確保返回的
supportedInterfaceOrientations
的相關(guān)值總類型 包含于preferredInterfaceOrientationForPresentation
返回的對應(yīng)類型值聊浅。 - 處理好
UINavigationController
中的上述五個屬性餐抢,理清topViewController
visibleViewController
以及 被present
出來的模態(tài)視圖控制器的isBeingPresented
和isBeingDismissed
屬性的含義。 - 處理好 基礎(chǔ)視圖控制器 中的
presentedViewController
及 理清其對應(yīng)的isBeingPresented
和isBeingDismissed
屬性的含義狗超。 - 【一個很重要的點忘記提及了】鍵盤彈出的布局方向和視圖控制器返回的supportedInterfaceOrientation是一致的弹澎,與你的狀態(tài)欄方向無關(guān)。
Happy 2018. Happy New Year!
有問題請在簡書中發(fā)送私信或者關(guān)注我的個人 微博努咐,給我留言苦蒿。謝謝關(guān)注,如果您有更多的想法渗稍,請聯(lián)系互相交流佩迟。
Demo 在此团滥,歡迎star!!!
TODO_List:
- 第三方APP調(diào)起時的相關(guān)配置稍后補上。