mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50231 Changelog: [Internal] RCTDeviceInfo uses KVO to listen to frame changes in the application's keyWindow. On initialization, it reads the global keyWindow and adds itself as a listener. When RCTDeviceInfo is cleaned up, it reads the global keyWindow again, and removes itself as an observer. However, this makes an assumption that the keyWindow is always the same. This is not always true - for example, when a UIAlert is presented, the OS creates a new temporary keyWindow to host the alert in order to make sure it is the first responder. If the cleanup is called then, the app will crash because there is no RCTDeviceInfo observing it. Another example is the LogBox, which also temporarily creates a new keyWindow. The fix is simple, we can capture a reference to the application's keyWindow on initialization, but make sure it is weakly held as the keyWindow is usually managed by iOS. Then, when we remove the listener, it is always guaranteed it is the window that we are observing. Reviewed By: javache, cipolleschi, realsoelynn Differential Revision: D71667722 fbshipit-source-id: 103baf980b79b413fb29e6c3deff81dae33671c0
306 lines
11 KiB
Plaintext
306 lines
11 KiB
Plaintext
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTDeviceInfo.h"
|
|
|
|
#import <FBReactNativeSpec/FBReactNativeSpec.h>
|
|
#import <React/RCTAccessibilityManager.h>
|
|
#import <React/RCTAssert.h>
|
|
#import <React/RCTConstants.h>
|
|
#import <React/RCTEventDispatcherProtocol.h>
|
|
#import <React/RCTInitializing.h>
|
|
#import <React/RCTInvalidating.h>
|
|
#import <React/RCTUtils.h>
|
|
#import <atomic>
|
|
|
|
#import "CoreModulesPlugins.h"
|
|
|
|
using namespace facebook::react;
|
|
|
|
@interface RCTDeviceInfo () <NativeDeviceInfoSpec, RCTInitializing, RCTInvalidating>
|
|
@end
|
|
|
|
@implementation RCTDeviceInfo {
|
|
UIInterfaceOrientation _currentInterfaceOrientation;
|
|
NSDictionary *_currentInterfaceDimensions;
|
|
BOOL _isFullscreen;
|
|
std::atomic<BOOL> _invalidated;
|
|
NSDictionary *_constants;
|
|
|
|
__weak UIWindow *_applicationWindow;
|
|
}
|
|
|
|
static NSString *const kFrameKeyPath = @"frame";
|
|
|
|
@synthesize moduleRegistry = _moduleRegistry;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
- (instancetype)init
|
|
{
|
|
if (self = [super init]) {
|
|
_applicationWindow = RCTKeyWindow();
|
|
[_applicationWindow addObserver:self forKeyPath:kFrameKeyPath options:NSKeyValueObservingOptionNew context:nil];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
|
ofObject:(id)object
|
|
change:(NSDictionary *)change
|
|
context:(void *)context
|
|
{
|
|
if ([keyPath isEqualToString:kFrameKeyPath]) {
|
|
[self interfaceFrameDidChange];
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RCTWindowFrameDidChangeNotification object:self];
|
|
}
|
|
}
|
|
|
|
+ (BOOL)requiresMainQueueSetup
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
return dispatch_get_main_queue();
|
|
}
|
|
|
|
- (void)initialize
|
|
{
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(didReceiveNewContentSizeMultiplier)
|
|
name:RCTAccessibilityManagerDidUpdateMultiplierNotification
|
|
object:[_moduleRegistry moduleForName:"AccessibilityManager"]];
|
|
|
|
_currentInterfaceDimensions = [self _exportedDimensions];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(interfaceOrientationDidChange)
|
|
name:UIApplicationDidBecomeActiveNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(interfaceFrameDidChange)
|
|
name:RCTUserInterfaceStyleDidChangeNotification
|
|
object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(interfaceFrameDidChange)
|
|
name:UIApplicationDidBecomeActiveNotification
|
|
object:nil];
|
|
|
|
#if TARGET_OS_IOS
|
|
|
|
_currentInterfaceOrientation = RCTKeyWindow().windowScene.interfaceOrientation;
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(interfaceFrameDidChange)
|
|
name:UIDeviceOrientationDidChangeNotification
|
|
object:nil];
|
|
#endif
|
|
|
|
// TODO T175901725 - Registering the RCTDeviceInfo module to the notification is a short-term fix to unblock 0.73
|
|
// The actual behavior should be that the module is properly registered in the TurboModule/Bridge infrastructure
|
|
// and the infrastructure imperatively invoke the `invalidate` method, rather than listening to a notification.
|
|
// This is a temporary workaround until we can investigate the issue better as there might be other modules in a
|
|
// similar situation.
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(invalidate)
|
|
name:RCTBridgeWillInvalidateModulesNotification
|
|
object:nil];
|
|
|
|
_constants = @{
|
|
@"Dimensions" : [self _exportedDimensions],
|
|
// Note:
|
|
// This prop is deprecated and will be removed in a future release.
|
|
// Please use this only for a quick and temporary solution.
|
|
// Use <SafeAreaView> instead.
|
|
@"isIPhoneX_deprecated" : @(RCTIsIPhoneNotched()),
|
|
};
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
if (_invalidated.exchange(YES)) {
|
|
return;
|
|
}
|
|
[self _cleanupObservers];
|
|
}
|
|
|
|
- (void)_cleanupObservers
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
name:RCTAccessibilityManagerDidUpdateMultiplierNotification
|
|
object:[_moduleRegistry moduleForName:"AccessibilityManager"]];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:RCTUserInterfaceStyleDidChangeNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:RCTBridgeWillInvalidateModulesNotification object:nil];
|
|
|
|
[_applicationWindow removeObserver:self forKeyPath:kFrameKeyPath];
|
|
|
|
#if TARGET_OS_IOS
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
|
|
#endif
|
|
}
|
|
|
|
static BOOL RCTIsIPhoneNotched()
|
|
{
|
|
static BOOL isIPhoneNotched = NO;
|
|
static dispatch_once_t onceToken;
|
|
|
|
#if TARGET_OS_IOS
|
|
dispatch_once(&onceToken, ^{
|
|
RCTAssertMainQueue();
|
|
|
|
// 20pt is the top safeArea value in non-notched devices
|
|
UIWindow *keyWindow = RCTKeyWindow();
|
|
if (keyWindow) {
|
|
isIPhoneNotched = keyWindow.safeAreaInsets.top > 20;
|
|
}
|
|
});
|
|
#endif
|
|
|
|
return isIPhoneNotched;
|
|
}
|
|
|
|
static NSDictionary *RCTExportedDimensions(CGFloat fontScale)
|
|
{
|
|
RCTAssertMainQueue();
|
|
UIScreen *mainScreen = UIScreen.mainScreen;
|
|
CGSize screenSize = mainScreen.bounds.size;
|
|
UIView *mainWindow = RCTKeyWindow();
|
|
|
|
// We fallback to screen size if a key window is not found.
|
|
CGSize windowSize = mainWindow ? mainWindow.bounds.size : screenSize;
|
|
|
|
NSDictionary<NSString *, NSNumber *> *dimsWindow = @{
|
|
@"width" : @(windowSize.width),
|
|
@"height" : @(windowSize.height),
|
|
@"scale" : @(mainScreen.scale),
|
|
@"fontScale" : @(fontScale)
|
|
};
|
|
|
|
NSDictionary<NSString *, NSNumber *> *dimsScreen = @{
|
|
@"width" : @(screenSize.width),
|
|
@"height" : @(screenSize.height),
|
|
@"scale" : @(mainScreen.scale),
|
|
@"fontScale" : @(fontScale)
|
|
};
|
|
return @{@"window" : dimsWindow, @"screen" : dimsScreen};
|
|
}
|
|
|
|
- (NSDictionary *)_exportedDimensions
|
|
{
|
|
RCTAssert(!_invalidated, @"Failed to get exported dimensions: RCTDeviceInfo has been invalidated");
|
|
RCTAssert(_moduleRegistry, @"Failed to get exported dimensions: RCTModuleRegistry is nil");
|
|
RCTAccessibilityManager *accessibilityManager =
|
|
(RCTAccessibilityManager *)[_moduleRegistry moduleForName:"AccessibilityManager"];
|
|
RCTAssert(accessibilityManager, @"Failed to get exported dimensions: AccessibilityManager is nil");
|
|
CGFloat fontScale = accessibilityManager ? accessibilityManager.multiplier : 1.0;
|
|
return RCTExportedDimensions(fontScale);
|
|
}
|
|
|
|
- (NSDictionary<NSString *, id> *)constantsToExport
|
|
{
|
|
return [self getConstants];
|
|
}
|
|
|
|
- (NSDictionary<NSString *, id> *)getConstants
|
|
{
|
|
return _constants;
|
|
}
|
|
|
|
- (void)didReceiveNewContentSizeMultiplier
|
|
{
|
|
__weak __typeof(self) weakSelf = self;
|
|
RCTModuleRegistry *moduleRegistry = _moduleRegistry;
|
|
RCTExecuteOnMainQueue(^{
|
|
// Report the event across the bridge.
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[[moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"didUpdateDimensions"
|
|
body:[weakSelf _exportedDimensions]];
|
|
#pragma clang diagnostic pop
|
|
});
|
|
}
|
|
|
|
- (void)interfaceOrientationDidChange
|
|
{
|
|
#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
|
|
UIApplication *application = RCTSharedApplication();
|
|
UIInterfaceOrientation nextOrientation = RCTKeyWindow().windowScene.interfaceOrientation;
|
|
|
|
BOOL isRunningInFullScreen =
|
|
CGRectEqualToRect(application.delegate.window.frame, application.delegate.window.screen.bounds);
|
|
// We are catching here two situations for multitasking view:
|
|
// a) The app is in Split View and the container gets resized -> !isRunningInFullScreen
|
|
// b) The app changes to/from fullscreen example: App runs in slide over mode and goes into fullscreen->
|
|
// isRunningInFullScreen != _isFullscreen The above two cases a || b can be shortened to !isRunningInFullScreen ||
|
|
// !_isFullscreen;
|
|
BOOL isResizingOrChangingToFullscreen = !isRunningInFullScreen || !_isFullscreen;
|
|
BOOL isOrientationChanging = (UIInterfaceOrientationIsPortrait(_currentInterfaceOrientation) &&
|
|
!UIInterfaceOrientationIsPortrait(nextOrientation)) ||
|
|
(UIInterfaceOrientationIsLandscape(_currentInterfaceOrientation) &&
|
|
!UIInterfaceOrientationIsLandscape(nextOrientation));
|
|
|
|
// Update when we go from portrait to landscape, or landscape to portrait
|
|
// Also update when the fullscreen state changes (multitasking) and only when the app is in active state.
|
|
if ((isOrientationChanging || isResizingOrChangingToFullscreen) && RCTIsAppActive()) {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"didUpdateDimensions"
|
|
body:[self _exportedDimensions]];
|
|
// We only want to track the current _currentInterfaceOrientation and _isFullscreen only
|
|
// when it happens and only when it is published.
|
|
_currentInterfaceOrientation = nextOrientation;
|
|
_isFullscreen = isRunningInFullScreen;
|
|
#pragma clang diagnostic pop
|
|
}
|
|
#endif
|
|
}
|
|
|
|
- (void)interfaceFrameDidChange
|
|
{
|
|
__weak __typeof(self) weakSelf = self;
|
|
RCTExecuteOnMainQueue(^{
|
|
[weakSelf _interfaceFrameDidChange];
|
|
});
|
|
}
|
|
|
|
- (void)_interfaceFrameDidChange
|
|
{
|
|
NSDictionary *nextInterfaceDimensions = [self _exportedDimensions];
|
|
|
|
// update and publish the even only when the app is in active state
|
|
if (!([nextInterfaceDimensions isEqual:_currentInterfaceDimensions]) && RCTIsAppActive()) {
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"didUpdateDimensions"
|
|
body:nextInterfaceDimensions];
|
|
// We only want to track the current _currentInterfaceOrientation only
|
|
// when it happens and only when it is published.
|
|
_currentInterfaceDimensions = nextInterfaceDimensions;
|
|
#pragma clang diagnostic pop
|
|
}
|
|
}
|
|
|
|
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
|
|
{
|
|
return std::make_shared<NativeDeviceInfoSpecJSI>(params);
|
|
}
|
|
|
|
@end
|
|
|
|
Class RCTDeviceInfoCls(void)
|
|
{
|
|
return RCTDeviceInfo.class;
|
|
}
|