diff --git a/Libraries/Utilities/Appearance.d.ts b/Libraries/Utilities/Appearance.d.ts index 7d2faf69d8..2b8428e0d7 100644 --- a/Libraries/Utilities/Appearance.d.ts +++ b/Libraries/Utilities/Appearance.d.ts @@ -28,6 +28,16 @@ export namespace Appearance { */ export function getColorScheme(): ColorSchemeName; + /** + * Set the color scheme preference. This is useful for overriding the default + * color scheme preference for the app. Note that this will not change the + * appearance of the system UI, only the appearance of the app. + * Only available on iOS 13+ and Android 10+. + */ + export function setColorScheme( + scheme: ColorSchemeName | null | undefined, + ): void; + /** * Add an event handler that is fired when appearance preferences change. */ diff --git a/Libraries/Utilities/Appearance.js b/Libraries/Utilities/Appearance.js index 54d03dd182..f08588774d 100644 --- a/Libraries/Utilities/Appearance.js +++ b/Libraries/Utilities/Appearance.js @@ -85,6 +85,19 @@ module.exports = { return nativeColorScheme; }, + setColorScheme(colorScheme: ?ColorSchemeName): void { + const nativeColorScheme = colorScheme == null ? 'unspecified' : colorScheme; + + invariant( + colorScheme === 'dark' || colorScheme === 'light' || colorScheme == null, + "Unrecognized color scheme. Did you mean 'dark', 'light' or null?", + ); + + if (NativeAppearance != null && NativeAppearance.setColorScheme != null) { + NativeAppearance.setColorScheme(nativeColorScheme); + } + }, + /** * Add an event handler that is fired when appearance preferences change. */ diff --git a/Libraries/Utilities/NativeAppearance.js b/Libraries/Utilities/NativeAppearance.js index cb8688f14e..786790f598 100644 --- a/Libraries/Utilities/NativeAppearance.js +++ b/Libraries/Utilities/NativeAppearance.js @@ -26,6 +26,7 @@ export interface Spec extends TurboModule { // types. /* 'light' | 'dark' */ +getColorScheme: () => ?string; + +setColorScheme?: (colorScheme: string) => void; // RCTEventEmitter +addListener: (eventName: string) => void; diff --git a/React/CoreModules/RCTAppearance.h b/React/CoreModules/RCTAppearance.h index d8bb18b89a..caa842d72f 100644 --- a/React/CoreModules/RCTAppearance.h +++ b/React/CoreModules/RCTAppearance.h @@ -8,6 +8,7 @@ #import #import +#import #import RCT_EXTERN void RCTEnableAppearancePreference(BOOL enabled); diff --git a/React/CoreModules/RCTAppearance.mm b/React/CoreModules/RCTAppearance.mm index 71259d4a98..72257c8fa1 100644 --- a/React/CoreModules/RCTAppearance.mm +++ b/React/CoreModules/RCTAppearance.mm @@ -10,6 +10,7 @@ #import #import #import +#import #import "CoreModulesPlugins.h" @@ -68,6 +69,20 @@ NSString *RCTColorSchemePreference(UITraitCollection *traitCollection) return RCTAppearanceColorSchemeLight; } +@implementation RCTConvert (UIUserInterfaceStyle) + +RCT_ENUM_CONVERTER( + UIUserInterfaceStyle, + (@{ + @"light" : @(UIUserInterfaceStyleLight), + @"dark" : @(UIUserInterfaceStyleDark), + @"unspecified" : @(UIUserInterfaceStyleUnspecified) + }), + UIUserInterfaceStyleUnspecified, + integerValue); + +@end + @interface RCTAppearance () @end @@ -92,6 +107,17 @@ RCT_EXPORT_MODULE(Appearance) return std::make_shared(params); } +RCT_EXPORT_METHOD(setColorScheme : (NSString *)style) +{ + UIUserInterfaceStyle userInterfaceStyle = [RCTConvert UIUserInterfaceStyle:style]; + NSArray<__kindof UIWindow *> *windows = RCTSharedApplication().windows; + if (@available(iOS 13.0, *)) { + for (UIWindow *window in windows) { + window.overrideUserInterfaceStyle = userInterfaceStyle; + } + } +} + RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getColorScheme) { if (_currentColorScheme == nil) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java index bda3ef7ec2..4dece29d17 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.java @@ -11,6 +11,7 @@ import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDelegate; import com.facebook.fbreact.specs.NativeAppearanceSpec; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; @@ -65,6 +66,17 @@ public class AppearanceModule extends NativeAppearanceSpec { return "light"; } + @Override + public void setColorScheme(String style) { + if (style.equals("dark")) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else if (style.equals("light")) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } else if (style.equals("unspecified")) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + } + } + @Override public String getColorScheme() { // Attempt to use the Activity context first in order to get the most up to date diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK index 4cb4e08194..044b4c8d09 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/BUCK @@ -14,6 +14,7 @@ rn_android_library( deps = [ react_native_dep("third-party/java/jsr-305:jsr-305"), react_native_dep("third-party/android/androidx:annotation"), + react_native_dep("third-party/android/androidx:appcompat"), react_native_target("java/com/facebook/react/bridge:bridge"), react_native_target("java/com/facebook/react/common:common"), react_native_target("java/com/facebook/react/module/annotations:annotations"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK index b64122b9d3..5621dd3176 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/BUCK @@ -15,6 +15,7 @@ rn_android_library( react_native_dep("libraries/fresco/fresco-react-native:imagepipeline"), react_native_dep("libraries/soloader/java/com/facebook/soloader:soloader"), react_native_dep("third-party/android/androidx:annotation"), + react_native_dep("third-party/android/androidx:appcompat"), react_native_dep("third-party/android/androidx:core"), react_native_dep("third-party/android/androidx:fragment"), react_native_dep("third-party/android/androidx:legacy-support-core-utils"), diff --git a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js index 3947321e94..f9ff4e6df0 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -10,7 +10,7 @@ import * as React from 'react'; import {useState, useEffect} from 'react'; -import {Appearance, Text, useColorScheme, View} from 'react-native'; +import {Appearance, Text, useColorScheme, View, Button} from 'react-native'; import type { AppearancePreferences, ColorSchemeName, @@ -135,6 +135,32 @@ const ColorShowcase = (props: {themeName: string}) => ( ); +const ToggleNativeAppearance = () => { + const [nativeColorScheme, setNativeColorScheme] = + useState(null); + const colorScheme = useColorScheme(); + + useEffect(() => { + Appearance.setColorScheme(nativeColorScheme); + }, [nativeColorScheme]); + + return ( + + Native colorScheme: {nativeColorScheme} + Current colorScheme: {colorScheme} +