1 import Foundation 2 import ExpoModulesCore 3 4 public protocol ScreenOrientationController: AnyObject { screenOrientationDidChangenull5 func screenOrientationDidChange(_ orientation: UIInterfaceOrientation) 6 } 7 8 /** 9 This singleton that holds information about desired orientation for every app which uses expo-screen-orientation. 10 Marked @objc and public, because this it is also used in EXAppViewController. 11 */ 12 @objc 13 public class ScreenOrientationRegistry: NSObject, UIApplicationDelegate { 14 @objc 15 public static let shared = ScreenOrientationRegistry() 16 17 public var currentScreenOrientation: UIInterfaceOrientation 18 var orientationControllers: [ScreenOrientationController] = [] 19 var controllerInterfaceMasks: [ObjectIdentifier: UIInterfaceOrientationMask] = [:] 20 @objc 21 public weak var currentTraitCollection: UITraitCollection? 22 var lastOrientationMask: UIInterfaceOrientationMask 23 var rootViewController: UIViewController? { 24 let keyWindow = UIApplication 25 .shared 26 .connectedScenes 27 .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } 28 .last { $0.isKeyWindow } 29 30 return keyWindow?.rootViewController 31 } 32 33 public var currentOrientationMask: UIInterfaceOrientationMask { 34 var currentOrientationMask: UIInterfaceOrientationMask = [] 35 36 EXUtilities.performSynchronously { 37 currentOrientationMask = self.rootViewController?.supportedInterfaceOrientations ?? [] 38 } 39 return currentOrientationMask 40 } 41 42 private override init() { 43 self.currentScreenOrientation = .unknown 44 self.currentTraitCollection = nil 45 self.lastOrientationMask = [] 46 47 super.init() 48 49 // This is most likely already executed on the main thread, but we need to be sure 50 RCTExecuteOnMainQueue { 51 UIDevice.current.beginGeneratingDeviceOrientationNotifications() 52 } 53 } 54 55 /** 56 Called by ScreenOrientationAppDelegate in order to set initial interface orientation. 57 */ 58 public func updateCurrentScreenOrientation() { 59 let windows = UIApplication.shared.windows 60 if !windows.isEmpty { 61 self.currentScreenOrientation = windows[0].windowScene?.interfaceOrientation ?? .unknown 62 } 63 } 64 65 deinit { 66 EXUtilities.performSynchronously { 67 UIDevice.current.endGeneratingDeviceOrientationNotifications() 68 } 69 } 70 71 // MARK: - Affecting screen orientation 72 73 /** 74 Rotates the view to currentScreenOrientation or default orientation from the orientationMask. 75 */ 76 @objc 77 public func enforceDesiredDeviceOrientation(withOrientationMask orientationMask: UIInterfaceOrientationMask) { 78 var newOrientation = orientationMask.defaultOrientation() 79 80 if orientationMask.contains(currentScreenOrientation) { 81 newOrientation = currentScreenOrientation 82 } 83 84 guard newOrientation != .unknown else { 85 return 86 } 87 88 RCTExecuteOnMainQueue { [weak self] in 89 guard let self = self else { 90 return 91 } 92 93 if #available(iOS 16.0, *) { 94 let windowScene = self.rootViewController?.view.window?.windowScene 95 windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask)) 96 self.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() 97 } else { 98 UIDevice.current.setValue(newOrientation.rawValue, forKey: "orientation") 99 UIViewController.attemptRotationToDeviceOrientation() 100 } 101 102 if self.currentScreenOrientation == .unknown { 103 // CurrentScreenOrientation might be unknown (especially just after launch), but at this point we already know it. 104 // Later the currentScreenOrientation will be updated by the iOS orientation change notifications. 105 self.screenOrientationDidChange(newOrientation) 106 } 107 } 108 } 109 110 public func setMask(_ mask: UIInterfaceOrientationMask, forController controller: any ScreenOrientationController) { 111 let controllerIdentifier = ObjectIdentifier(controller) 112 113 controllerInterfaceMasks[controllerIdentifier] = mask 114 enforceDesiredDeviceOrientation(withOrientationMask: mask) 115 } 116 117 // MARK: - Getters 118 119 /** 120 Gets the orientationMask for the app. Uses an intersection of all applied orientation masks. Also used for Expo Go in EXAppViewController. 121 */ 122 @objc 123 public func requiredOrientationMask() -> UIInterfaceOrientationMask { 124 if controllerInterfaceMasks.isEmpty { 125 return [] 126 } 127 128 // We want to apply an orientation mask which is an intersection of locks applied by the modules. 129 var mask = doesDeviceHaveNotch ? UIInterfaceOrientationMask.allButUpsideDown : UIInterfaceOrientationMask.all 130 131 for moduleMask in controllerInterfaceMasks { 132 mask = mask.intersection(moduleMask.value) 133 } 134 135 return mask 136 } 137 138 // MARK: - Events 139 140 /** 141 Called by ScreenOrientationViewController when the dimensions of the view change. 142 Also used for Expo Go in EXAppViewController. 143 */ 144 @objc 145 public func viewDidTransition(toOrientation orientation: UIInterfaceOrientation) { 146 let currentDeviceOrientation = UIDevice.current.orientation.toInterfaceOrientation() 147 let currentOrientationMask = self.rootViewController?.supportedInterfaceOrientations ?? [] 148 149 var newScreenOrientation = UIInterfaceOrientation.unknown 150 151 // We need to deduce what is the new screen orientaiton based on currentOrientationMask and new dimensions of the view 152 if orientation.isPortrait { 153 // From trait collection, we know that screen is in portrait or upside down orientation. 154 let portraitMask = currentOrientationMask.intersection([.portrait, .portraitUpsideDown]) 155 156 if portraitMask == .portrait { 157 // Mask allows only proper portrait - we know that the device is in either proper portrait or upside down 158 // we deduce it is proper portrait. 159 newScreenOrientation = .portrait 160 } else if portraitMask == .portraitUpsideDown { 161 // Mask allows only upside down portrait - we know that the device is in either proper portrait or upside down 162 // we deduce it is upside down portrait. 163 newScreenOrientation = .portraitUpsideDown 164 } else if currentDeviceOrientation == .portrait || currentDeviceOrientation == .portraitUpsideDown { 165 // Mask allows portrait or upside down portrait - we can try to deduce orientation 166 // from device orientation. 167 newScreenOrientation = currentDeviceOrientation 168 } 169 } else if orientation.isLandscape { 170 // From trait collection, we know that screen is in landscape left or right orientation. 171 let landscapeMask = currentOrientationMask.intersection(.landscape) 172 173 if landscapeMask == .landscapeLeft { 174 // Mask allows only proper landscape - we know that the device is in either proper landscape left or right 175 // we deduce it is proper left. 176 newScreenOrientation = .landscapeLeft 177 } else if landscapeMask == .landscapeRight { 178 // Mask allows only landscape right - we know that the device is in either proper landscape left or right 179 // we deduce it is landscape right. 180 newScreenOrientation = .landscapeRight 181 } else if currentDeviceOrientation == .landscapeLeft || currentDeviceOrientation == .landscapeRight { 182 // Mask allows landscape left or right - we can try to deduce orientation 183 // from device orientation. 184 newScreenOrientation = currentDeviceOrientation 185 } else if currentDeviceOrientation == .portrait || currentDeviceOrientation == .portraitUpsideDown { 186 // If the desired orientation is .landscape but the device is in .portrait orientation it will rotate to .landscapeRight 187 newScreenOrientation = .landscapeRight 188 } 189 } 190 screenOrientationDidChange(newScreenOrientation) 191 } 192 193 @objc 194 public func traitCollectionDidChange(to traitCollection: UITraitCollection) { 195 currentTraitCollection = traitCollection 196 } 197 198 /** 199 Called at the end of the screen orientation change. Notifies the controllers about the orientation change. 200 */ 201 func screenOrientationDidChange(_ newScreenOrientation: UIInterfaceOrientation) { 202 currentScreenOrientation = newScreenOrientation 203 204 for controller in orientationControllers { 205 controller.screenOrientationDidChange(newScreenOrientation) 206 } 207 } 208 209 public func registerController(_ controller: ScreenOrientationController) { 210 orientationControllers.append(controller) 211 } 212 213 public func unregisterController(_ controller: ScreenOrientationController) { 214 let controllerIdentifier = ObjectIdentifier(controller) 215 216 controllerInterfaceMasks.removeValue(forKey: controllerIdentifier) 217 orientationControllers.removeAll(where: { $0 === controller }) 218 } 219 } 220