Skip to content

Commit

Permalink
[iOS] Refactor expo-screen-orientation for versioning (expo#23228)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsapeta authored Jul 3, 2023
1 parent fb518d1 commit b027412
Show file tree
Hide file tree
Showing 11 changed files with 55 additions and 331 deletions.
5 changes: 3 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,7 @@ PODS:
- ABI49_0_0ExpoScreenOrientation (6.0.1):
- ABI49_0_0ExpoModulesCore
- ABI49_0_0React-Core
- ExpoScreenOrientation
- RCT-Folly (= 2021.07.22.00)
- ABI49_0_0ExpoSecureStore (12.3.0):
- ABI49_0_0ExpoModulesCore
Expand Down Expand Up @@ -4812,7 +4813,7 @@ SPEC CHECKSUMS:
ABI49_0_0ExpoNetwork: 028f02f5d15379f89e529b37982559e445f86ab9
ABI49_0_0ExpoPrint: 8065c3f088efcad86befd6ead6c2bc8adb425f24
ABI49_0_0ExpoRandom: 83829ccecffd4afbdd4f711e053639e2109df0b6
ABI49_0_0ExpoScreenOrientation: 246531771c12e546098bfbac9fb44b0efc174830
ABI49_0_0ExpoScreenOrientation: 708f0a5e64698ac2775e227ca0b14418a37fda30
ABI49_0_0ExpoSecureStore: 05b26374e173878efda9267822d64825c80f652e
ABI49_0_0ExpoSharing: e915ee89f06fe8f6985bf35a5a854c44612f34d5
ABI49_0_0ExpoSMS: 78e004b964755de5f25d07c5ad1aa09d968723ad
Expand Down Expand Up @@ -5055,6 +5056,6 @@ SPEC CHECKSUMS:
Yoga: 1d6727ed193122f6adaf435c3de1a768326ff83b
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb

PODFILE CHECKSUM: e93fdba4b82928a5067623f67dca0603beb4d28e
PODFILE CHECKSUM: 491e3a079001c45b62dcc853ab4973929f96a0c3

COCOAPODS: 1.12.1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"2021.07.22.00"
],
"ABI49_0_0ExpoModulesCore": [],
"ABI49_0_0React-Core": []
"ABI49_0_0React-Core": [],
"ExpoScreenOrientation": []
},
"compiler_flags": "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32",
"pod_target_xcconfig": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ABI49_0_0ExpoModulesCore

@objc(ABI49_0_0EXScreenOrientationAppDelegate)
public class ScreenOrientationAppDelegate: ExpoAppDelegateSubscriber {
public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
ScreenOrientationRegistry.shared.updateCurrentScreenOrientation()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ABI49_0_0ExpoModulesCore

public class ScreenOrientationModule: Module, OrientationListener, Hashable {
public class ScreenOrientationModule: Module, ScreenOrientationController {
static let didUpdateDimensionsEvent = "expoDidUpdateDimensions"

let screenOrientationRegistry = ScreenOrientationRegistry.shared
Expand All @@ -22,7 +22,7 @@ public class ScreenOrientationModule: Module, OrientationListener, Hashable {
throw UnsupportedOrientationLockException(orientationLock)
}

screenOrientationRegistry.setMask(orientationMask, forModule: self)
screenOrientationRegistry.setMask(orientationMask, forController: self)
}

AsyncFunction("lockPlatformAsync") { (allowedOrientations: [ModuleOrientation]) in
Expand All @@ -43,7 +43,7 @@ public class ScreenOrientationModule: Module, OrientationListener, Hashable {
throw UnsupportedOrientationLockException(nil)
}

screenOrientationRegistry.setMask(allowedOrientationsMask, forModule: self)
screenOrientationRegistry.setMask(allowedOrientationsMask, forController: self)
}

AsyncFunction("getOrientationLockAsync") {
Expand Down Expand Up @@ -75,23 +75,18 @@ public class ScreenOrientationModule: Module, OrientationListener, Hashable {
return ModuleOrientation.from(orientation: screenOrientationRegistry.currentScreenOrientation).rawValue
}

OnStartObserving {
screenOrientationRegistry.registerModuleToReceiveNotification(self)
}

OnStopObserving {
screenOrientationRegistry.unregisterModuleFromReceivingNotification(self)
OnCreate {
screenOrientationRegistry.registerController(self)
}

OnDestroy {
screenOrientationRegistry.unregisterModuleFromReceivingNotification(self)
screenOrientationRegistry.moduleWillDeallocate(self)
screenOrientationRegistry.unregisterController(self)
}
}

// MARK: - ScreenOrientationListener
// MARK: - ScreenOrientationController

func screenOrientationDidChange(_ orientation: UIInterfaceOrientation) {
public func screenOrientationDidChange(_ orientation: UIInterfaceOrientation) {
guard let currentTraitCollection = screenOrientationRegistry.currentTraitCollection else {
return
}
Expand All @@ -105,14 +100,4 @@ public class ScreenOrientationModule: Module, OrientationListener, Hashable {
] as [String: Any]
])
}

// MARK: - Hashable

public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}

public static func == (lhs: ScreenOrientationModule, rhs: ScreenOrientationModule) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ABI49_0_0ExpoModulesCore

@objc(ABI49_0_0EXScreenOrientationReactDelegateHandler)
public class ScreenOrientationReactDelegateHandler: ExpoReactDelegateHandler {
public override func createRootViewController(reactDelegate: ExpoReactDelegate) -> UIViewController? {
return ScreenOrientationViewController(defaultScreenOrientationFromPlist: ())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,258 +1,6 @@
import Foundation
import ABI49_0_0ExpoModulesCore
// The original implementations of `ScreenOrientationRegistry` and `ScreenOrientationController`
// were removed from this file as part of the versioning process to always use their "unversioned" version.
import ExpoScreenOrientation

protocol OrientationListener {
func screenOrientationDidChange(_ orientation: UIInterfaceOrientation)
}

/**
This singleton that holds information about desired orientation for every app which uses expo-screen-orientation.
Marked @objc and public, because this it is also used in ABI49_0_0EXAppViewController.
*/
@objc
public class ScreenOrientationRegistry: NSObject, UIApplicationDelegate {
@objc
public static let shared = ScreenOrientationRegistry()

var currentScreenOrientation: UIInterfaceOrientation
var orientationListeners: [ScreenOrientationModule?] = []
var moduleInterfaceMasks: [ScreenOrientationModule: UIInterfaceOrientationMask] = [:]
weak var currentTraitCollection: UITraitCollection?
var lastOrientationMask: UIInterfaceOrientationMask
var rootViewController: UIViewController? {
let keyWindow = UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.last { $0.isKeyWindow }

return keyWindow?.rootViewController
}

var currentOrientationMask: UIInterfaceOrientationMask {
var currentOrientationMask: UIInterfaceOrientationMask = []

ABI49_0_0EXUtilities.performSynchronously {
currentOrientationMask = self.rootViewController?.supportedInterfaceOrientations ?? []
}
return currentOrientationMask
}

private override init() {
self.currentScreenOrientation = .unknown
self.currentTraitCollection = nil
self.lastOrientationMask = []

super.init()

NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleDeviceOrientationChange(notification:)),
name: UIDevice.orientationDidChangeNotification,
object: UIDevice.current
)

// This is most likely already executed on the main thread, but we need to be sure
ABI49_0_0RCTExecuteOnMainQueue {
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
}
}

/**
Called by ScreenOrientationAppDelegate in order to set initial interface orientation.
*/
func updateCurrentScreenOrientation() {
let windows = UIApplication.shared.windows
if !windows.isEmpty {
self.currentScreenOrientation = windows[0].windowScene?.interfaceOrientation ?? .unknown
}
}

deinit {
ABI49_0_0EXUtilities.performSynchronously {
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
}

// MARK: - Affecting screen orientation

/**
Rotates the view to currentScreenOrientation or default orientation from the orientationMask.
*/
func enforceDesiredDeviceOrientation(withOrientationMask orientationMask: UIInterfaceOrientationMask) {
var newOrientation = orientationMask.defaultOrientation()

if orientationMask.contains(currentScreenOrientation) {
newOrientation = currentScreenOrientation
}

guard newOrientation != .unknown else {
return
}

ABI49_0_0RCTExecuteOnMainQueue { [weak self] in
guard let self = self else {
return
}

if #available(iOS 16.0, *) {
let windowScene = self.rootViewController?.view.window?.windowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientationMask))
self.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
} else {
UIDevice.current.setValue(newOrientation.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
}

if self.currentScreenOrientation == .unknown {
// CurrentScreenOrientation might be unknown (especially just after launch), but at this point we already know it.
// Later the currentScreenOrientation will be updated by the iOS orientation change notifications.
self.screenOrientationDidChange(newOrientation)
}
}
}

func setMask(_ mask: UIInterfaceOrientationMask, forModule module: ScreenOrientationModule) {
moduleInterfaceMasks[module] = mask
enforceDesiredDeviceOrientation(withOrientationMask: mask)
}

// MARK: - Getters

/**
Gets the orientationMask for the app. Uses an intersection of all applied orientation masks. Also used for Expo Go in ABI49_0_0EXAppViewController.
*/
@objc
public func requiredOrientationMask() -> UIInterfaceOrientationMask {
if moduleInterfaceMasks.isEmpty {
return []
}

// We want to apply an orientation mask which is an intersection of locks applied by the modules.
var mask = doesDeviceHaveNotch ? UIInterfaceOrientationMask.allButUpsideDown : UIInterfaceOrientationMask.all

for moduleMask in moduleInterfaceMasks {
mask = mask.intersection(moduleMask.value)
}

return mask
}

// MARK: - Events

/**
Called when the OS sends an OrientationDidChange notification.
*/
@objc
func handleDeviceOrientationChange(notification: Notification) {
let newScreenOrientation = UIDevice.current.orientation.toInterfaceOrientation()

interfaceOrientationDidChange(newScreenOrientation)
}

/**
Called when the device is physically rotated. Checks if screen orientation should be changed after user rotated the device.
*/
func interfaceOrientationDidChange(_ newScreenOrientation: UIInterfaceOrientation) {
if currentScreenOrientation == newScreenOrientation || newScreenOrientation == .unknown {
return
}

if currentOrientationMask.contains(newScreenOrientation) {
// when changing orientation without changing dimensions traitCollectionDidChange isn't triggered so the event has to be called manually
if (newScreenOrientation.isPortrait && currentScreenOrientation.isPortrait)
|| (newScreenOrientation.isLandscape && currentScreenOrientation.isLandscape) {
screenOrientationDidChange(newScreenOrientation)
return
}

// on iPads, traitCollectionDidChange isn't triggered at all, so we have to call screenOrientationDidChange manually
if isPad()
&& (newScreenOrientation.isPortrait && currentScreenOrientation.isLandscape
|| newScreenOrientation.isLandscape && currentScreenOrientation.isPortrait) {
screenOrientationDidChange(newScreenOrientation)
}
}
}

/**
Called by ScreenOrientationViewController when the dimensions of the view change.
Also used for Expo Go in ABI49_0_0EXAppViewController.
*/
@objc
public func traitCollectionDidChange(to traitCollection: UITraitCollection) {
currentTraitCollection = traitCollection

let currentDeviceOrientation = UIDevice.current.orientation.toInterfaceOrientation()
let currentOrientationMask = self.rootViewController?.supportedInterfaceOrientations ?? []

var newScreenOrientation = UIInterfaceOrientation.unknown

// We need to deduce what is the new screen orientaiton based on currentOrientationMask and new dimensions of the view
if traitCollection.isPortrait() {
// From trait collection, we know that screen is in portrait or upside down orientation.
let portraitMask = currentOrientationMask.intersection([.portrait, .portraitUpsideDown])

if portraitMask == .portrait {
// Mask allows only proper portrait - we know that the device is in either proper portrait or upside down
// we deduce it is proper portrait.
newScreenOrientation = .portrait
} else if portraitMask == .portraitUpsideDown {
// Mask allows only upside down portrait - we know that the device is in either proper portrait or upside down
// we deduce it is upside down portrait.
newScreenOrientation = .portraitUpsideDown
} else if currentDeviceOrientation == .portrait || currentDeviceOrientation == .portraitUpsideDown {
// Mask allows portrait or upside down portrait - we can try to deduce orientation
// from device orientation.
newScreenOrientation = currentDeviceOrientation
}
} else if traitCollection.isLandscape() {
// From trait collection, we know that screen is in landscape left or right orientation.
let landscapeMask = currentOrientationMask.intersection(.landscape)

if landscapeMask == .landscapeLeft {
// Mask allows only proper landscape - we know that the device is in either proper landscape left or right
// we deduce it is proper left.
newScreenOrientation = .landscapeLeft
} else if landscapeMask == .landscapeRight {
// Mask allows only landscape right - we know that the device is in either proper landscape left or right
// we deduce it is landscape right.
newScreenOrientation = .landscapeRight
} else if currentDeviceOrientation == .landscapeLeft || currentDeviceOrientation == .landscapeRight {
// Mask allows landscape left or right - we can try to deduce orientation
// from device orientation.
newScreenOrientation = currentDeviceOrientation
} else if currentDeviceOrientation == .portrait || currentDeviceOrientation == .portraitUpsideDown {
// If the desired orientation is .landscape but the device is in .portrait orientation it will rotate to .landscapeRight
newScreenOrientation = .landscapeRight
}
}
screenOrientationDidChange(newScreenOrientation)
}

/**
Called at the end of the screen orientation change. Notifies modules about the orientation change.
*/
func screenOrientationDidChange(_ newScreenOrientation: UIInterfaceOrientation) {
currentScreenOrientation = newScreenOrientation
for module in orientationListeners {
module?.screenOrientationDidChange(newScreenOrientation)
}
}

func moduleWillDeallocate(_ module: ScreenOrientationModule) {
moduleInterfaceMasks.removeValue(forKey: module)
}

func registerModuleToReceiveNotification(_ module: ScreenOrientationModule) {
orientationListeners.append(module)
}

func unregisterModuleFromReceivingNotification(_ module: ScreenOrientationModule) {
for i in (0..<orientationListeners.count).reversed() {
if orientationListeners[i] === module || orientationListeners[i] == nil {
orientationListeners.remove(at: i)
}
}
}
}
typealias ScreenOrientationRegistry = ExpoScreenOrientation.ScreenOrientationRegistry
typealias ScreenOrientationController = ExpoScreenOrientation.ScreenOrientationController
2 changes: 2 additions & 0 deletions packages/expo-screen-orientation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 💡 Others

- [iOS] Refactor the singleton class to work properly in versioned code in Expo Go. ([#23228](https://github.com/expo/expo/pull/23228) by [@tsapeta](https://github.com/tsapeta))

## 6.0.1 — 2023-06-23

### 🐛 Bug fixes
Expand Down
Loading

0 comments on commit b027412

Please sign in to comment.