One, foreword

Swift, version 4.0

Xcode version 9.2

In the previous project requirements, almost all vertical screen display interface, so I also save trouble, directly in the TARGETS interface direction option only tick vertical screen, so as to meet the requirements.

However, in a recent project, the product suddenly added a requirement that part of the interface support rotation, which led to the study of the screen rotation problem!

Those who need urgent solutions go straight to 3.3


Two, screen rotation related knowledge

2.1 Understanding and connection of the three directions

  • UIDeviceOrientation: Device orientation
public enum UIDeviceOrientation : Int {
    case unknown
    case portrait // Device vertically, the home button is below
    case portraitUpsideDown // The home button is on the top of the device
    case landscapeLeft This example demonstrates the horizontally orientation of the device, with the home button on the right
    case landscapeRight This example demonstrates the horizontally orientation of the device, with the home button on the left
    case faceUp // The device is flat with the screen facing up
    case faceDown // The device is flat with the screen facing down
}
Copy the code

The meaning of this enumeration can be seen from the naming of device directions, which in this case refer to the direction of the physical device (i.e., the iPhone).

  • UIInterfaceOrientation: interface orientation
public enum UIInterfaceOrientation : Int {
    case unknown
    case portrait
    case portraitUpsideDown
    case landscapeLeft
    case landscapeRight
}
Copy the code

Orientation refers to the orientation of the content displayed on the screen, which is the same as the orientation of the Home button. You can understand UIDeviceOrientation and UIInterfaceOrientation if you look at the screen rotation, we turn the phone to the left, and you can see the screen goes to the right.

  • UIInterfaceOrientationMask:Is used to control the direction in which it is allowed to turnUIInterfaceOrientation
public struct UIInterfaceOrientationMask : OptionSet {
    public init(rawValue: UInt)
    public static var portrait: UIInterfaceOrientationMask { get }
    public static var landscapeLeft: UIInterfaceOrientationMask { get }
    public static var landscapeRight: UIInterfaceOrientationMask { get }
    public static var portraitUpsideDown: UIInterfaceOrientationMask { get }
    public static var landscape: UIInterfaceOrientationMask { get }
    public static var all: UIInterfaceOrientationMask { get }
    public static var allButUpsideDown: UIInterfaceOrientationMask { get}}Copy the code

2.2 Observe screen rotation and respond

2.2.1 Observe the device direction and respond

 // No notification was generated
if !UIDevice.current.isGeneratingDeviceOrientationNotifications {
    // Generate a notification
     UIDevice.current.beginGeneratingDeviceOrientationNotifications()
}

// Lock portrait, still valid, such as faceUp.
NotificationCenter.default.addObserver(self, 
                                        selector: #selector(handleDeviceOrientationChange(notification:)),
                                        name:NSNotification.Name.UIDeviceOrientationDidChange,
                                        object: nil)
Copy the code
@objc private func handleDeviceOrientationChange(notification: Notification) {
    // Get device direction
    let orientation = UIDevice.current.orientation
    switch orientation {
        case .landscapeRight:
            / / iOS8, landscape UIScreen. Main. Bounds. When the width is equal to the vertical screen UIScreen. Main. Bounds. Height
            print(UIScreen.main.bounds.width)
            print("landscapeRight")
        default: break}}Copy the code

The cancellation

deinit {
    NotificationCenter.default.removeObserver(self)
    UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
Copy the code

2.2.2 Observe the interface direction and respond

And it’s similar to that but the name of the observation is theta

// Lock portrait, invalid, notification method does not fire
NSNotification.Name.UIApplicationWillChangeStatusBarOrientation
NSNotification.Name.UIApplicationDidChangeStatusBarOrientation
Copy the code

Obtaining interface Direction

let statusBarOrientation = UIApplication.shared.statusBarOrientation
Copy the code

2.2.3 advice

It is recommended to monitor the interface direction for two reasons:

  • Listen for device direction, returns multiple directions, for exampleportraitandfaceUpDon’t conflict.
  • Listen to the device orientation, as mentioned above, first the device rotates, then the interface rotates, there is a problem here, when we operate the interface, maybe the interface hasn’t rotated yet.

Three, problem solving actual combat

Firstly, we need to configure TARGETS for Device Orientation. Here is the main switch, and the direction shown in the figure is selected by default:

Protrait

This configuration should not conflict with the direction of code control, or it will cause a crash.


3.1 Functions to control screen rotation

// Defaults to true
override var shouldAutorotate: Bool {
    return true
}
// The supported rotation direction
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return .landscapeLeft
}
// The default direction for mode switching
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
    return .landscapeRight
}
Copy the code

All three of these properties override UIViewController properties. I studied mode switching for a while before, but I didn’t write a summary, I will write it later (:).

And these three methods are affected by the controller hierarchy, that is, if the current controller configuration supports rotation, if its navigation controller, or even Tabbar controller, does not support rotation, the current controller configuration will not take effect.


3.2 Solution under different root controllers

Core problem: Few screens need to be rotated, and most need to be locked portrait.

3.2.1 The root controller is UIViewController

Corresponding Demo configuration:

BaseVC

class BaseVC: UIViewController {

    override var shouldAutorotate: Bool {
        return false
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
    }
    
    override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
        return .portrait
    }
    
    override func viewDidLoad(a) {
        super.viewDidLoad()

    }
    
}
Copy the code

The rest of the controllers then inherit BaseVC, and the controller that needs to rotate overrides the method again individually.


3.2.2 The root controller is UINavigationController

Corresponding Demo configuration:

UINavigationController

class BaseNavC: UINavigationController {

    override var shouldAutorotate: Bool {
        return self.viewControllers.last? .shouldAutorotate ??false
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return self.viewControllers.last? .supportedInterfaceOrientations ?? .portrait }override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
        return self.viewControllers.last? .preferredInterfaceOrientationForPresentation ?? .portrait }override func viewDidLoad(a) {
        super.viewDidLoad()

    }

}
Copy the code

3.2.3 The root controller is UITabBarController

Corresponding Demo configuration:

class BaseTabBarC: UITabBarController {

    override var shouldAutorotate: Bool {
        return self.selectedViewController? .shouldAutorotate ??false
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return self.selectedViewController? .supportedInterfaceOrientations ?? .portrait }override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
        return self.selectedViewController? .preferredInterfaceOrientationForPresentation ?? .portrait }override func viewDidLoad(a) {
        super.viewDidLoad()

    }

}
Copy the code

Similarly, we just need to get the configuration of the currently selected controller and assign it to the UITabBarController, layer by layer!


3.3 The simplest implementation

Corresponding Demo configuration:

Appdelegate

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) 
-> UIInterfaceOrientationMask{}Copy the code

Then I immediately came up with a super easy way to do this: define a global variable or cache a bool as follows:

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) 
-> UIInterfaceOrientationMask {
    if isAllowAutorotate {
        return [.portrait, .landscapeLeft, .landscapeRight]
    }
    else {
        return .portrait
    }
}
Copy the code

Then default the isAllowAutorotate global variable to false in the controller that needs to be rotated:

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        isAllowAutorotate = true
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        isAllowAutorotate = false}}Copy the code

So you don’t have to bother with all that inheritance stuff!

3.4 Supplementary (reply to comment questions)

First of all thanks to @superdanny in the comments for pointing this out

I was being thoughtless here. By the way, to discuss the question I wanted to write but didn’t write:

A. Portrait jump B. Portrait jumpCopy the code

The following points to discuss:

  • 1. If it isPresentMode switch

For a and b two cases, we can rewrite directly into the controller preferredInterfaceOrientationForPresentation properties, while the visual effects don’t look so good. Case A is rare, but case B is still common, such as jumping into the controller of a full-screen video player, where you can customize the mode-switching animation or rotate the animation directly.

override var preferredInterfaceOrientationForPresentation:
UIInterfaceOrientation {
    return .portrait
}
Copy the code
  • 2. If it isPushswitch

In the incoming controller:

override var shouldAutorotate: Bool {
    return true
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // Switch to the direction you want
    driveScreen(to: .portrait)
}

func driveScreen(to direction: UIInterfaceOrientation) {
    UIDevice.current.setValue(direction.rawValue, forKey: "orientation")}Copy the code

It is worth noting that remember rewrite supportedInterfaceOrientations setting allows the direction of the jump, Demo has been updated.

In general, use these methods flexibly to achieve the purpose required by the product, and don’t forget to customize the mode switch animation or direct rotation animation can also achieve the visual effect of interface rotation, although the direction is still portrait!

4. Postscript and Demo

Github ScreenRotationDemo

Reference:

IOS Record 11: Code handling iOS horizontal and vertical rotation

How to force view controller orientation in iOS 8?