【iOS】 橫豎屏 旋轉(zhuǎn) 解決方案 - Swift

本文基于 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)出未找到解決方案時饱溢,想拍案而起抓狂的場景喧伞。

案例場景

場景案例圖示.png

圖有點大,可以打開一個新標(biāo)簽放大查看绩郎,我們項目APP的一個大概的結(jié)構(gòu)圖潘鲫,主要指示了一下涉及到旋轉(zhuǎn)屏的視圖控制器,以及各個控制器之間的關(guān)系肋杖,是Push出來的還是Present出來的溉仑。

簡單描述一下場景:

  1. 主視圖控制器是一個繼承自 UITabBarController 的視圖控制器。
  2. 底部有四個Tab状植,四個Tab分別指向繼承自 UINavigationController 的視圖控制器作為根視圖浊竟。
  3. 通常情況下,都是豎屏津畸,四個Tab的部分界面中都有跳播放器視圖控制器的入口振定。
  4. 進播放器時,有兩種方式進入肉拓,豎屏 or 橫屏后频。
  5. 第一次是默認(rèn)豎屏,之后進入時暖途,由用戶最后退出播放器時的閱讀方向來決定徘郭。
  6. 播放器中有四個菜單和一個評論輸入框。
  7. 點擊 評論輸入框丧肴,彈出一個可輸入評論的視圖控制器残揉,以 present 的形式彈出,會覆蓋在播放器之上芋浮,并且能看到后面的播放器內(nèi)容抱环。方向與當(dāng)前閱讀器的方向一致。
  8. 點擊 目錄纸巷,以 push 的方式打開目錄頁镇草。目錄頁方向與播放器方向一致。(之前的需求是目錄頁要以豎屏的方式出現(xiàn)瘤旨,當(dāng)然梯啤,這個也可以實現(xiàn),下面會說解決方案)
  9. 點擊 旋轉(zhuǎn) 菜單存哲,切換播放器方向因宇,豎屏 -> 橫屏七婴,or 橫屏 -> 豎屏
  10. 用戶在輸入評論之后察滑,點擊右邊或者鍵盤的的 發(fā)送 按鈕打厘,會先判斷當(dāng)前用戶的登錄狀態(tài),如果未登錄或者登錄信息失效贺辰,會 present 一個 豎屏登錄界面户盯。
  11. 登錄界面 同樣包裝在一個 UINavigationController 之中,用戶未注冊時還可以 push 到一個 注冊 界面饲化,同樣也是豎屏莽鸭,第三方登錄方式有 微信QQ吃靠,微博 等硫眨。
  12. 播放器可以被外部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.

  1. 通過 UIApplication.shared.statusBarOrientation 獲取和設(shè)置判耕,還有另外一個方法來設(shè)置這個屬性的值透绩,可以傳遞動畫與否的參數(shù),UIApplication.shared.setStatusBarOrientation(:, animated: ),直接設(shè)置這個屬性值壁熄,相當(dāng)于調(diào)用了該方法時傳入了 animated: false,即不使用任何動畫形式來改變狀態(tài)欄的方向帚豪。
  2. 如果你的程序中的某個視圖控制器的界面是可旋轉(zhuǎn)的,那么你不應(yīng)該隨意的去設(shè)置這個屬性草丧,意圖改變狀態(tài)欄的方向狸臣,因為這將可能無效。(我就曾遇到過昌执,邏輯都是從另外一個項目中照搬過來的烛亦,但是調(diào)用此方法時,死活不改變方向仙蚜。當(dāng)然此洲,這跟你是否正確的返回 shouldAutorotate有關(guān)系,下面會講到委粉。)
  3. 作為總結(jié)呜师,如果你的當(dāng)前視圖控制器的 shouldAutorotate返回 true,則盡量不要再去調(diào)用 UIApplication.shared.statusBarOrientation 了, 一是可能無效,二是 statusBarOrientation的方向會隨著你返回的supportedInterfaceOrientation 改變而自動改變贾节。

正題

按照官方的說法汁汗,我打算一步一步的告訴大家衷畦,如何配置,如何編寫代碼知牌,從最根部祈争,到最外層。

  1. 首先角寸,配置程序的info.plist配置文件菩混,只勾選豎屏,這樣可以保證豎屏啟動界面 (即 LaunchScreen.storyboard 配置的程序默認(rèn)啟動界面在任何情況下都豎屏啟動)扁藕。

    程序Info.plist的配置

  2. AppDelegate 中的配置:

     @UIApplicationMain
     class AppDelegate: UIResponder, UIApplicationDelegate {
         ...
         func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
             return .allButUpsideDown
         }
         ...
     }
    
    • 當(dāng)然沮峡,如果你的程序支持 iPad ,可以返回 .all 來支持所有的方向亿柑。
    • 一般情況下邢疙,返回 .allButUpsideDown 就夠了。
    • 前面講到過望薄,UIKit 會取視圖控制器返回的值和當(dāng)前返回的值疟游,做一個交叉,取交叉值痕支,所有這里返回最大范圍的支持方向颁虐。
  3. 自定義五個基類,分別是:

    • 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
                }
            }
        
        }
      
  4. 再來添加另外一個 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)閉了,體驗不好峭竣。

  1. 配置 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)要求我們返回的 supportedInterfaceOrientationspreferredInterfaceOrientationForPresentation 至少要有可交叉的值亚脆,UIInterfaceOrientation 只能定義一個值,UIInterfaceOrientationMask 支持 OptionSet 協(xié)議 可返回一個數(shù)組盲泛,因此可以是多個值濒持,所以可做如上處理,避免你沒有重寫 preferredInterfaceOrientationForPresentation 由系統(tǒng)返回的默認(rèn)值 或者 你重寫了寺滚,但是由于代碼邏輯錯誤柑营,返回了一個與 supportedInterfaceOrientations 方向不一致的值。

  1. 配置 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 讥蟆、視圖控制器的 presentedViewControllerpresentingViewController 是什么概念纺阔,那么建議百度 or Google 一下再看下面的內(nèi)容瘸彤,這里就不做普及了,以免篇幅過長笛钝。

    1. 判斷導(dǎo)航控制器棧頂?shù)囊晥D控制器 topViewController 是否有 presentedViewController质况,如果有,并且正在被 present 當(dāng)中玻靡,則優(yōu)先使用該 presentedViewController 的配置參數(shù)结榄。
    2. 判斷導(dǎo)航控制器棧頂?shù)囊晥D控制器 topViewController 是否有 presentedViewController,如果有囤捻,并且正在被 dismiss 當(dāng)中臼朗,則優(yōu)先使用該 topViewController 的配置參數(shù)。
    3. 剩下的是默認(rèn)配置蝎土,不再判斷有沒有 presentedViewController ,也不再判斷 presentedViewController 的狀態(tài)视哑,由系統(tǒng)決定。是使用 presentedViewController 還是使用 topViewController誊涯。
    4. 左滑返回手勢是否開啟由兩個原則挡毅,一是如果視圖控制器返回的 isForbidInteractivePopGesturetrue 時禁用,二是 默認(rèn)判斷 視圖控制器的堆棧中視圖控制器的數(shù)量暴构,大于 1 時可用跪呈。
  2. 兩大容器類型的視圖控制器重寫完了,接下來我們來寫其他三個取逾。

  3. 配置 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ù)制黏貼...怕大家漏寫又來一通問沐序,一通罵琉用,怎么不行呀!片紙!!!!片紙!!!! ...策幼,下面還是說一下處理邏輯:

    1. 如果存在 presentedViewController ,并且正在被 present晶丘,則優(yōu)先使用 presentedViewController 的配置參數(shù)。
    2. 如果存在 presentedViewController 沫浆,并且正在被 dismiss滚秩,則優(yōu)先使用當(dāng)前控制器的參數(shù)配置郁油,如果子類沒有重寫對應(yīng)的系列 _xxx_ 方法,則使用默認(rèn)參數(shù)拄显。
    3. 如果存在 presentedViewController (說明它當(dāng)前正在被顯示)案站,則優(yōu)先使用 presentedViewController 的配置參數(shù)。
    4. 最后盒件,使用子類自定義(如果子類有重寫對應(yīng)的系列 _xxx_ 方法)或默認(rèn)配置舱禽。
  4. 配置 BaseTableViewController:

     class BaseTableViewController: UITableViewControlelr {
         
         // 和 BaseViewController 中一模一樣的代碼,直接黏貼過來即可誊稚。
     
     }
    
  5. 配置 BaseCollectionViewController:

     class BaseTableViewController: UITableViewControlelr {
         
         // 和 BaseViewController 中一模一樣的代碼里伯,直接黏貼過來即可。
     
     }
    
  6. 五大基礎(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)用無效的情況堰怨。

  7. 播放器視圖控制器 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)
            }
            
            ...
            ...
        }
    
    }
    

    播放器大概的配置就這些,也很簡單,主要的注意點在于:

    1. 控制好變量 _isLandscape 的傳入時機零院,一定要在視圖控制器進入之前傳入,建議是構(gòu)造視圖控制器時就傳入告抄。
    2. viewDidLoadviewWillAppear 都執(zhí)行 updateOrientationIfNeeded 方法打洼。
    3. 通過 _isLandscape 控制 _supportedInterfaceOrientations__preferredInterfaceOrientationForPresentation_ 的返回值募疮。
  8. 評論輸入框界面 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 差不多姜胖,需要注意的一點是:

    1. 因為界面是 present 出來的,并且不自定義轉(zhuǎn)場動畫時隧出,需要配置 modalPresentationStylemodalTransitionStyle,轉(zhuǎn)場樣式可以自己指定饲鄙,modalPresentationStyle 目前我沒有使用 .custom 模式,使用 overFullScreen 問題相對少一點。
    2. 如果你的界面中也存在需要半透明或者透明度的部分轴咱,則需要把視圖控制器的 viewbackgroundColor 設(shè)置成透明,然后自己加一層黑色背景的控件窖剑,用一個 alpha 動畫漸變到小于1.0的某個值戈稿。
  9. 目錄 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
        }
    
    }
    

    基本和上面的兩個類的配置一致。

  10. 登錄 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)航控制器的 modalPresentationStylemodalTransitionStyle 的配置。modalPresentationStyle 一定設(shè)置成 .fullScreen, 不過這個是系統(tǒng)默認(rèn)設(shè)置野来,這里只是保險起見恼除。modalTransitionStyle 一般情況下,登錄 界面都是以 .coverVertical 的形式出現(xiàn)的曼氛。


最后

最后的最后豁辉,做一個簡單的總結(jié)。

  1. 五個跟旋轉(zhuǎn)屏舀患,狀態(tài)欄樣式有關(guān)系的屬性徽级,從根視圖控制器一路傳到最頂級視圖。分別是:
    • prefersStatusBarHidden
    • preferredStatusBarStyle
    • shouldAutorotate
    • supportedInterfaceOrientations
    • preferredInterfaceOrientationForPresentation
  2. 確保返回的 supportedInterfaceOrientations 的相關(guān)值總類型 包含于 preferredInterfaceOrientationForPresentation 返回的對應(yīng)類型值聊浅。
  3. 處理好 UINavigationController 中的上述五個屬性餐抢,理清 topViewController visibleViewController 以及 被 present出來的模態(tài)視圖控制器的 isBeingPresentedisBeingDismissed 屬性的含義。
  4. 處理好 基礎(chǔ)視圖控制器 中的 presentedViewController 及 理清其對應(yīng)的 isBeingPresentedisBeingDismissed 屬性的含義狗超。
  5. 【一個很重要的點忘記提及了】鍵盤彈出的布局方向和視圖控制器返回的supportedInterfaceOrientation是一致的弹澎,與你的狀態(tài)欄方向無關(guān)。

Happy 2018. Happy New Year!

有問題請在簡書中發(fā)送私信或者關(guān)注我的個人 微博努咐,給我留言苦蒿。謝謝關(guān)注,如果您有更多的想法渗稍,請聯(lián)系互相交流佩迟。


Demo 在此团滥,歡迎star!!!

TODO_List:

  1. 第三方APP調(diào)起時的相關(guān)配置稍后補上。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末报强,一起剝皮案震驚了整個濱河市灸姊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秉溉,老刑警劉巖力惯,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異召嘶,居然都是意外死亡父晶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門弄跌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甲喝,“玉大人,你說我怎么就攤上這事铛只〔号郑” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵淳玩,是天一觀的道長直撤。 經(jīng)常有香客問我,道長凯肋,這世上最難降的妖魔是什么谊惭? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮侮东,結(jié)果婚禮上圈盔,老公的妹妹穿的比我還像新娘。我一直安慰自己悄雅,他們只是感情好驱敲,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宽闲,像睡著了一般众眨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上容诬,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天娩梨,我揣著相機與錄音,去河邊找鬼览徒。 笑死狈定,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纽什,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼措嵌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了芦缰?” 一聲冷哼從身側(cè)響起企巢,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎让蕾,沒想到半個月后浪规,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡涕俗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年罗丰,在試婚紗的時候發(fā)現(xiàn)自己被綠了神帅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片再姑。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖找御,靈堂內(nèi)的尸體忽然破棺而出元镀,到底是詐尸還是另有隱情,我是刑警寧澤霎桅,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布栖疑,位于F島的核電站,受9級特大地震影響滔驶,放射性物質(zhì)發(fā)生泄漏遇革。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一揭糕、第九天 我趴在偏房一處隱蔽的房頂上張望萝快。 院中可真熱鬧,春花似錦著角、人聲如沸揪漩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奄容。三九已至,卻和暖如春产徊,著一層夾襖步出監(jiān)牢的瞬間昂勒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工舟铜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留戈盈,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓深滚,卻偏偏與公主長得像奕谭,于是被迫代替她去往敵國和親涣觉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 第一步 首先保證工程支持橫豎屏 不多說看圖 保證圈紅的地方 打?qū)?58F678EC-EABC-4320-9FCB...
    ylgwhyh閱讀 1,783評論 0 1
  • 概述 摘要:從制作一個看圖app和了解關(guān)鍵概念開始swift編程血柳。 概念:Constants and variab...
    lbhw閱讀 454評論 0 1
  • SwiftDay011.MySwiftimport UIKitprintln("Hello Swift!")var...
    smile麗語閱讀 3,826評論 0 6
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理官册,服務(wù)發(fā)現(xiàn),斷路器难捌,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • 天地未分之時,被稱為混沌狀態(tài)击敌。天地乾坤混在一起介返,日月星辰?jīng)]有生成,晝夜寒暑沒有交替出現(xiàn)沃斤,上面沒有風(fēng)雨雷電圣蝎,下面沒有...
    Alones閱讀 2,578評論 0 3