From 43b51474ab4180e8de3d6a3d240d9080f05be0f4 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 13 Oct 2025 14:32:04 -0700 Subject: [PATCH 1/4] requiresMainQueueSetup YES on macOS --- packages/react-native/React/CoreModules/RCTAppearance.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-native/React/CoreModules/RCTAppearance.mm b/packages/react-native/React/CoreModules/RCTAppearance.mm index 4053d54a9e1a12..677eb5687da85a 100644 --- a/packages/react-native/React/CoreModules/RCTAppearance.mm +++ b/packages/react-native/React/CoreModules/RCTAppearance.mm @@ -134,7 +134,11 @@ - (instancetype)init + (BOOL)requiresMainQueueSetup { +#if !TARGET_OS_OSX // [macOS] return NO; +#else // [macOS + return YES; +#endif // macOS] } - (dispatch_queue_t)methodQueue From c412ffa7abd9935537cb07927a4043215ca55ed0 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 13 Oct 2025 15:18:06 -0700 Subject: [PATCH 2/4] fix: implement RCTAppearanceProxy --- .../Base/UIKitProxies/RCTAppearanceProxy.h | 28 ++++++ .../Base/UIKitProxies/RCTAppearanceProxy.mm | 92 +++++++++++++++++++ .../UIKitProxies/RCTInitializeUIKitProxies.mm | 7 +- .../React/CoreModules/RCTAppearance.mm | 45 +++++---- .../React/CoreModules/RCTDeviceInfo.mm | 4 + 5 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 packages/react-native/React/Base/UIKitProxies/RCTAppearanceProxy.h create mode 100644 packages/react-native/React/Base/UIKitProxies/RCTAppearanceProxy.mm diff --git a/packages/react-native/React/Base/UIKitProxies/RCTAppearanceProxy.h b/packages/react-native/React/Base/UIKitProxies/RCTAppearanceProxy.h new file mode 100644 index 00000000000000..c3886a109ac5ee --- /dev/null +++ b/packages/react-native/React/Base/UIKitProxies/RCTAppearanceProxy.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#if TARGET_OS_OSX // [macOS +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTAppearanceProxy : NSObject + ++ (instancetype)sharedInstance; + +/* + * Property to access the current appearance. + * Thread safe. + */ +@property (nonatomic, readonly) NSAppearance *currentAppearance; + +- (void)startObservingAppearance; + +@end + +NS_ASSUME_NONNULL_END +#endif // macOS] diff --git a/packages/react-native/React/Base/UIKitProxies/RCTAppearanceProxy.mm b/packages/react-native/React/Base/UIKitProxies/RCTAppearanceProxy.mm new file mode 100644 index 00000000000000..9e25b3604f996f --- /dev/null +++ b/packages/react-native/React/Base/UIKitProxies/RCTAppearanceProxy.mm @@ -0,0 +1,92 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#if TARGET_OS_OSX // [macOS +#import "RCTAppearanceProxy.h" + +#import +#import + +#import + +@implementation RCTAppearanceProxy { + BOOL _isObserving; + std::mutex _mutex; + NSAppearance *_currentAppearance; +} + ++ (instancetype)sharedInstance +{ + static RCTAppearanceProxy *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [RCTAppearanceProxy new]; + }); + return sharedInstance; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _isObserving = NO; + _currentAppearance = [NSApp effectiveAppearance]; + } + return self; +} + +- (void)startObservingAppearance +{ + RCTAssertMainQueue(); + std::lock_guard lock(_mutex); + if (!_isObserving) { + _isObserving = YES; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_appearanceDidChange:) + name:RCTUserInterfaceStyleDidChangeNotification + object:nil]; + } +} + +- (NSAppearance *)currentAppearance +{ + { + std::lock_guard lock(_mutex); + if (_isObserving) { + return _currentAppearance; + } + } + + __block NSAppearance *appearance = nil; + if (RCTIsMainQueue()) { + appearance = [NSApp effectiveAppearance]; + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + appearance = [NSApp effectiveAppearance]; + }); + } + return appearance; +} + +- (void)_appearanceDidChange:(NSNotification *)notification +{ + std::lock_guard lock(_mutex); + + NSDictionary *userInfo = [notification userInfo]; + if (userInfo) { + NSAppearance *appearance = userInfo[RCTUserInterfaceStyleDidChangeNotificationAppearanceKey]; + if (appearance != nil) { + _currentAppearance = appearance; + return; + } + } + + _currentAppearance = [NSApp effectiveAppearance]; +} + +@end +#endif // macOS] diff --git a/packages/react-native/React/Base/UIKitProxies/RCTInitializeUIKitProxies.mm b/packages/react-native/React/Base/UIKitProxies/RCTInitializeUIKitProxies.mm index 09dfb43d4ab2da..4e059fab360340 100644 --- a/packages/react-native/React/Base/UIKitProxies/RCTInitializeUIKitProxies.mm +++ b/packages/react-native/React/Base/UIKitProxies/RCTInitializeUIKitProxies.mm @@ -10,6 +10,9 @@ #import "RCTKeyWindowValuesProxy.h" #import "RCTTraitCollectionProxy.h" #import "RCTWindowSafeAreaProxy.h" +#if TARGET_OS_OSX // [macOS +#import "RCTAppearanceProxy.h" +#endif // macOS] void RCTInitializeUIKitProxies(void) { @@ -19,7 +22,9 @@ void RCTInitializeUIKitProxies(void) #if !TARGET_OS_OSX // [macOS] [[RCTTraitCollectionProxy sharedInstance] startObservingTraitCollection]; [[RCTInitialAccessibilityValuesProxy sharedInstance] recordAccessibilityValues]; -#endif // [macOS] +#else // [macOS + [[RCTAppearanceProxy sharedInstance] startObservingAppearance]; +#endif // macOS] [[RCTKeyWindowValuesProxy sharedInstance] startObservingWindowSizeIfNecessary]; }); } diff --git a/packages/react-native/React/CoreModules/RCTAppearance.mm b/packages/react-native/React/CoreModules/RCTAppearance.mm index 677eb5687da85a..5f8fe515dac383 100644 --- a/packages/react-native/React/CoreModules/RCTAppearance.mm +++ b/packages/react-native/React/CoreModules/RCTAppearance.mm @@ -14,6 +14,12 @@ #import "CoreModulesPlugins.h" +#if TARGET_OS_OSX // [macOS +#import + +#import "RCTAppearanceProxy.h" +#endif // macOS] + using namespace facebook::react; NSString *const RCTAppearanceColorSchemeLight = @"light"; @@ -119,7 +125,7 @@ - (instancetype)init UITraitCollection *traitCollection = [RCTTraitCollectionProxy sharedInstance].currentTraitCollection; _currentColorScheme = RCTColorSchemePreference(traitCollection); #else // [macOS - NSAppearance *appearance = RCTSharedApplication().appearance; + NSAppearance *appearance = [RCTAppearanceProxy sharedInstance].currentAppearance; _currentColorScheme = RCTColorSchemePreference(appearance); #endif // macOS] [[NSNotificationCenter defaultCenter] addObserver:self @@ -164,13 +170,15 @@ - (dispatch_queue_t)methodQueue window.overrideUserInterfaceStyle = userInterfaceStyle; } #else // [macOS - NSAppearance *appearance = nil; - if ([style isEqualToString:@"light"]) { - appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; - } else if ([style isEqualToString:@"dark"]) { - appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; - } - RCTSharedApplication().appearance = appearance; + RCTExecuteOnMainQueue(^{ + NSAppearance *appearance = nil; + if ([style isEqualToString:@"light"]) { + appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + } else if ([style isEqualToString:@"dark"]) { + appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + } + RCTSharedApplication().appearance = appearance; + }); #endif // macOS] } @@ -181,10 +189,7 @@ - (dispatch_queue_t)methodQueue UITraitCollection *traitCollection = [RCTTraitCollectionProxy sharedInstance].currentTraitCollection; _currentColorScheme = RCTColorSchemePreference(traitCollection); #else // [macOS - __block NSAppearance *appearance = nil; - RCTUnsafeExecuteOnMainQueueSync(^{ - appearance = RCTKeyWindow().appearance; - }); + NSAppearance *appearance = [RCTAppearanceProxy sharedInstance].currentAppearance; _currentColorScheme = RCTColorSchemePreference(appearance); #endif // macOS] } @@ -194,24 +199,24 @@ - (dispatch_queue_t)methodQueue - (void)appearanceChanged:(NSNotification *)notification { +#if !TARGET_OS_OSX // [macOS NSDictionary *userInfo = [notification userInfo]; -#if !TARGET_OS_OSX // [macOS] UITraitCollection *traitCollection = nil; if (userInfo) { traitCollection = userInfo[RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey]; } NSString *newColorScheme = RCTColorSchemePreference(traitCollection); -#else // [macOS - NSAppearance *appearance = nil; - if (userInfo) { - appearance = userInfo[RCTUserInterfaceStyleDidChangeNotificationAppearanceKey]; + if (![_currentColorScheme isEqualToString:newColorScheme]) { + _currentColorScheme = newColorScheme; + [self sendEventWithName:@"appearanceChanged" body:@{ @"colorScheme" : newColorScheme }]; } - NSString *newColorScheme = RCTColorSchemePreference(appearance); -#endif // macOS] +#else // [macOS + NSString *newColorScheme = RCTColorSchemePreference([RCTAppearanceProxy sharedInstance].currentAppearance); if (![_currentColorScheme isEqualToString:newColorScheme]) { _currentColorScheme = newColorScheme; - [self sendEventWithName:@"appearanceChanged" body:@{@"colorScheme" : newColorScheme}]; + [self sendEventWithName:@"appearanceChanged" body:@{ @"colorScheme" : newColorScheme }]; } +#endif // macOS] } #pragma mark - RCTEventEmitter diff --git a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm index 17e2d983daf850..3dce6f0b53060a 100644 --- a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm +++ b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm @@ -50,7 +50,11 @@ - (instancetype)init + (BOOL)requiresMainQueueSetup { +#if !TARGET_OS_OSX // [macOS] return NO; +#else // [macOS + return YES; +#endif // macOS] } - (dispatch_queue_t)methodQueue From 87e247920e1e149589422e1baccf03f8de6af7fa Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 14 Oct 2025 07:07:56 -0700 Subject: [PATCH 3/4] Update RCTAppearance.mm Co-authored-by: Tommy Nguyen <4123478+tido64@users.noreply.github.com> --- packages/react-native/React/CoreModules/RCTAppearance.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/React/CoreModules/RCTAppearance.mm b/packages/react-native/React/CoreModules/RCTAppearance.mm index 5f8fe515dac383..462d8fce970096 100644 --- a/packages/react-native/React/CoreModules/RCTAppearance.mm +++ b/packages/react-native/React/CoreModules/RCTAppearance.mm @@ -189,7 +189,7 @@ - (dispatch_queue_t)methodQueue UITraitCollection *traitCollection = [RCTTraitCollectionProxy sharedInstance].currentTraitCollection; _currentColorScheme = RCTColorSchemePreference(traitCollection); #else // [macOS - NSAppearance *appearance = [RCTAppearanceProxy sharedInstance].currentAppearance; + NSAppearance *appearance = [RCTAppearanceProxy sharedInstance].currentAppearance; _currentColorScheme = RCTColorSchemePreference(appearance); #endif // macOS] } From e3ef26eae8c318c248827de612ac8a4d9434d37b Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 14 Oct 2025 07:09:26 -0700 Subject: [PATCH 4/4] Update RCTAppearance.mm --- packages/react-native/React/CoreModules/RCTAppearance.mm | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTAppearance.mm b/packages/react-native/React/CoreModules/RCTAppearance.mm index 462d8fce970096..857c18d6ce5650 100644 --- a/packages/react-native/React/CoreModules/RCTAppearance.mm +++ b/packages/react-native/React/CoreModules/RCTAppearance.mm @@ -206,17 +206,13 @@ - (void)appearanceChanged:(NSNotification *)notification traitCollection = userInfo[RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey]; } NSString *newColorScheme = RCTColorSchemePreference(traitCollection); - if (![_currentColorScheme isEqualToString:newColorScheme]) { - _currentColorScheme = newColorScheme; - [self sendEventWithName:@"appearanceChanged" body:@{ @"colorScheme" : newColorScheme }]; - } #else // [macOS NSString *newColorScheme = RCTColorSchemePreference([RCTAppearanceProxy sharedInstance].currentAppearance); +#endif // macOS] if (![_currentColorScheme isEqualToString:newColorScheme]) { _currentColorScheme = newColorScheme; [self sendEventWithName:@"appearanceChanged" body:@{ @"colorScheme" : newColorScheme }]; } -#endif // macOS] } #pragma mark - RCTEventEmitter