From c18566ffdb44103a3e24cd8017d0ae6a69c68e40 Mon Sep 17 00:00:00 2001 From: Birkir Gudjonsson Date: Tue, 28 Feb 2023 05:28:38 -0800 Subject: [PATCH] Appearance.setColorScheme support (revisited) (#36122) Summary: Both Android and iOS allow you to set application specific user interface style, which is useful for applications that support both light and dark mode. With the newly added `Appearance.setColorScheme`, you can natively manage the application's user interface style rather than keeping that preference in JavaScript. The benefit is that native dialogs like alert, keyboard, action sheets and more will also be affected by this change. Implemented using Android X [AppCompatDelegate.setDefaultNightMode](https://developer.android.com/reference/androidx/appcompat/app/AppCompatDelegate#setDefaultNightMode(int)) and iOS 13+ [overrideUserInterfaceStyle](https://developer.apple.com/documentation/uikit/uiview/3238086-overrideuserinterfacestyle?language=objc) ```tsx // Lets assume a given device is set to **dark** mode. Appearance.getColorScheme(); // `dark` // Set the app's user interface to `light` Appearance.setColorScheme('light'); Appearance.getColorScheme(); // `light` // Set the app's user interface to `unspecified` Appearance.setColorScheme(null); Appearance.getColorScheme() // `dark` ``` ## Changelog [GENERAL] [ADDED] - Added `setColorScheme` to `Appearance` module Pull Request resolved: https://github.com/facebook/react-native/pull/36122 Test Plan: Added a RNTester for the feature in the Appearance section. Three buttons for toggling all set of modes. Reviewed By: lunaleaps Differential Revision: D43331405 Pulled By: NickGerleman fbshipit-source-id: 3b15f1ed0626d1ad7a8266ec026e903cd3ec46aa --- Libraries/Utilities/Appearance.d.ts | 10 ++++++ Libraries/Utilities/Appearance.js | 13 +++++++ Libraries/Utilities/NativeAppearance.js | 1 + React/CoreModules/RCTAppearance.h | 1 + React/CoreModules/RCTAppearance.mm | 26 ++++++++++++++ .../modules/appearance/AppearanceModule.java | 12 +++++++ .../facebook/react/modules/appearance/BUCK | 1 + .../main/java/com/facebook/react/shell/BUCK | 1 + .../examples/Appearance/AppearanceExample.js | 35 ++++++++++++++++++- 9 files changed, 99 insertions(+), 1 deletion(-) diff --git a/Libraries/Utilities/Appearance.d.ts b/Libraries/Utilities/Appearance.d.ts index 7d2faf69d8cec4..2b8428e0d7d1de 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 54d03dd18200b2..f08588774d585c 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 cb8688f14e2967..786790f5987140 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 d8bb18b89ac32b..caa842d72f6a1b 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 71259d4a98d119..72257c8fa1bfdc 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 @@ void RCTOverrideAppearancePreference(NSString *const colorSchemeOverride) 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 @@ - (dispatch_queue_t)methodQueue 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 bda3ef7ec2bd73..4dece29d17fc52 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.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 @@ private String colorSchemeForCurrentConfiguration(Context context) { 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 4cb4e081946767..044b4c8d0941a1 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 b64122b9d3975e..5621dd31762a68 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 3947321e946656..f9ff4e6df0a044 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} +