Skip to content

Commit

Permalink
feat: allow mounting multiple AppBackground components (#30)
Browse files Browse the repository at this point in the history
* chore: update deps

* chore: update docs

* chore: prettier

* chore: fix comment

* feat: allow mounting multiple AppBackground

* docs: readme

* docs: readme
  • Loading branch information
vonovak authored May 27, 2024
1 parent 1bf7dc4 commit a95955e
Show file tree
Hide file tree
Showing 32 changed files with 4,501 additions and 5,071 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ lib/

docgen/
theme-ex/

# for future https://github.com/yarnpkg/berry/issues/454#issuecomment-530312089
.yarn/*
!.yarn/releases
!.yarn/plugins
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ Control the native platform theme from React Native.

✅ Control theme of RN Views, as well as native UI controls (alerts, native menus, date pickers... on both Android and iOS)

✅ Control the color of the ApplicationWindow (iOS) or the current Activity (Android) using [`AppBackground`](https://github.com/vonovak-org/react-native-theme-control/blob/main/docs/readme-internal.md#appbackground)
✅ Control the color of the UIApplication Window (iOS) and the current Activity (Android) using [`AppBackground`](https://github.com/vonovak-org/react-native-theme-control/blob/main/docs/readme-internal.md#appbackground)

✅ Recover the theme upon app startup
✅ Recover the previously set theme upon app startup, and apply it before your app starts rendering

✅ Supports Expo via a config plugin

[New Architecture](https://reactnative.dev/docs/the-new-architecture/landing-page) supported

Additionally, provides functionality to control the appearance (background and border color, light / dark buttons) of the Android navbar.

The package is tested with RN >= 0.66.1 (last tested with RN 73, Expo 49). See [RN version support](docs/install.md).
The package is tested with RN >= 0.66.1 (last tested with RN 74, Expo 51). See [RN version support](docs/install.md).

### Example

Expand Down
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
presets: ['@react-native/babel-preset'],
};
103 changes: 92 additions & 11 deletions docs/install.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @vonovak/react-native-theme-control

iOS 13 or newer is required for theming logic. However, the package builds with iOS 10 and newer.
iOS 13 or newer is required for theming logic. However, the package builds with iOS 12 and newer.

Version 4 supports RN 0.72 and Expo 49.

Expand Down Expand Up @@ -64,7 +64,7 @@ There are manual installation steps that need to be performed:

### Android

- in `MainApplication.java`:
- in `MainApplication.kt / java`:

```diff
+ import eu.reactnativetraining.ThemeControlModule;
Expand All @@ -77,7 +77,6 @@ public void onCreate() {
+ ThemeControlModule.Companion.recoverApplicationTheme(getApplicationContext());

SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
```

Expand All @@ -93,20 +92,101 @@ If you want, `androidxCoreVersion` can be set to the version of the androidx cor

### iOS

## for RN >= 0.71
#### Add header search paths

The AppDelegate no longer contains the code that we need to modify to make theming work. Instead, we need to modify the `RCTAppDelegate.mm` file located in `node_modules/react-native/Libraries/AppDelegate`.
This step usually isn't necessary, but may be required when you use `use_frameworks!` in your Podfile.

Use the same modification as shown below for RN 0.70, and apply it to the node_modules file. Use tools such as `yarn patch` or `patch-package` to maintain the change.
In that case, we need to add a header search path to the `React-RCTAppDelegate` project target. This can be done in the Podfile:

## for RN <= 0.70
```
target 'YourApp' do
use_frameworks! :linkage => :static
config = use_native_modules!
# ...
post_install do |installer|
installer.pods_project.targets.each do |target|
if ["React-RCTAppDelegate"].any? { |t| t == target.name }
# PODS_ROOT points to ios/Pods
append_header_search_path(target, "$(PODS_ROOT)/some_path/node_modules/@vonovak/react-native-theme-control/ios")
end
end
end
end
# https://github.com/software-mansion/react-native-svg/issues/2081#issuecomment-1656701180
def append_header_search_path(target, path)
target.build_configurations.each do |config|
# Note that there's a space character after `$(inherited)`.
config.build_settings["HEADER_SEARCH_PATHS"] ||= "$(inherited) "
config.build_settings["HEADER_SEARCH_PATHS"] << path
end
end
```

#### Modify the AppDelegate

Recovering the application theme involves modification of native files. The following is required:

<details>
<summary>for RN >= 0.71</summary>

We need to modify the `RCTAppDelegate.mm` file located in `node_modules/react-native/Libraries/AppDelegate/RCTAppDelegate.mm`.

Use the modification shown below and apply it to the file in node_modules. Use tools such as `yarn patch` or `patch-package` to maintain the change. While it is a bit unusual to patch this file, you will get an efficient theme-switching solution (certainly better than loading the app, waiting to read theme from asyncStorage and re-drawing).

- in `AppDelegate.m`
This is a snippet from a RN 0.74 project:

```diff
+ #import "RNThemeControl.h"
+// use the one you need
+#if __has_include(<RNThemeControl.h>)
+ #import <RNThemeControl.h>
+#else
+ #import "RNThemeControl.h"
+#endif

(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// ...
if (self.newArchEnabled || self.fabricEnabled) {
[RCTComponentViewFactory currentComponentViewFactory].thirdPartyFabricComponentsProvider = self;
}
[self _logWarnIfCreateRootViewWithBridgeIsOverridden];
[self customizeRootView:(RCTRootView *)rootView];

//...
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [self createRootViewController];


+ [RNThemeControl recoverApplicationTheme];
+ // or use this for testing
+ [RNThemeControl forceTheme:UIUserInterfaceStyleDark];

[self setRootView:rootView toRootViewController:rootViewController];
self.window.rootViewController = rootViewController;
self.window.windowScene.delegate = self;
[self.window makeKeyAndVisible];

return YES;
}
```
</details>

<details>
<summary>for RN <= 0.70</summary>

- in `AppDelegate.m` make the following changes:

```diff
+// use the one you need
+#if __has_include(<RNThemeControl.h>)
+ #import <RNThemeControl.h>
+#else
+ #import "RNThemeControl.h"
+#endif

// ...
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
Expand All @@ -118,8 +198,9 @@ Use the same modification as shown below for RN 0.70, and apply it to the node_m
return YES;
}
```
</details>

If you want to force dark / light theme always, [prefer editing the plist file](https://stackoverflow.com/a/58034262/2070942) or use:
If you want to force dark / light theme always, [prefer editing the plist file](https://stackoverflow.com/a/58034262/2070942) or use for testing:

`[RNThemeControl forceTheme:UIUserInterfaceStyleDark];`

Expand Down
41 changes: 39 additions & 2 deletions docs/readme-internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- [setThemePreference](readme-internal.md#setthemepreference)
- [getThemePreference](readme-internal.md#getthemepreference)
- [useThemePreference](readme-internal.md#usethemepreference)
- [setNavbarAppearance](readme-internal.md#setnavbarappearance)
- [setAppBackground](readme-internal.md#setappbackground)

### React components

Expand Down Expand Up @@ -49,6 +51,34 @@ can be `system`, `light` or `dark`.

Function that returns the current theme preference

### setNavbarAppearance

> **setNavbarAppearance**(`params`): `Promise`\<`null`\>
Set the appearance of the navigation bar imperatively

## Parameters

**\_params**: [`NavbarAppearanceParams`](../type-aliases/NavbarAppearanceParams.md)

## Returns

`Promise`\<`null`\>

### setAppBackground

> **setAppBackground**(`bgColor`): `Promise`\<`boolean`\>
Set the application background imperatively

## Parameters

**bgColor**: `ColorValue`

## Returns

`Promise`\<`boolean`\>

___

### useThemePreference
Expand All @@ -67,7 +97,10 @@ ___

Android-only component, which controls the navigation bar appearance: the background color, divider color and whether the navbar buttons are light or dark.
If active color scheme is dark, then the button icons will be rendered as light by default. You can override this behavior by passing a custom `barStyle` prop.
If you want to control the appearance imperatively, call `NavigationBar.setNavbarAppearance()`.

Multiple `NavigationBar` components can be mounted in the app, and always the last one will be used (different screens of your app can have different appearance).

If you want to control the appearance imperatively, call `setNavbarAppearance()`.

- `dark-content` means dark icons on a light navigation bar
- `light-content` means light icons on a dark navigation bar
Expand Down Expand Up @@ -107,11 +140,15 @@ ___

**AppBackground**(`props`): ``null``

Sets the background color of the ApplicationWindow (iOS) or the current Activity (Android).
Sets the background color of the UIApplication window (iOS) or the current Activity (Android).
This is useful with React Navigation to prevent [white flashes when navigating](https://github.com/react-navigation/react-navigation/issues/10951) on Android, or to control the background color users see when presenting a modal on iOS.

You need to specify the background color for light and dark mode separately.

Multiple `AppBackground` components can be mounted in the app, and always the last one will be used (different screens of your app can have different appearance).

If you want to control the appearance imperatively, call `setAppBackground()`.

#### Parameters

| Name | Type |
Expand Down
23 changes: 20 additions & 3 deletions example/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
buildscript {
def androidTestAppDir = "../../node_modules/react-native-test-app/android"
apply(from: "${androidTestAppDir}/dependencies.gradle")
apply(from: {
def searchDir = rootDir.toPath()
do {
def p = searchDir.resolve("node_modules/react-native-test-app/android/dependencies.gradle")
if (p.toFile().exists()) {
return p.toRealPath().toString()
}
} while (searchDir = searchDir.getParent())
throw new GradleException("Could not find `react-native-test-app`");
}())

repositories {
mavenCentral()
Expand All @@ -18,7 +26,16 @@ allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url("${rootDir}/../../node_modules/react-native/android")
url({
def searchDir = rootDir.toPath()
do {
def p = searchDir.resolve("node_modules/react-native/android")
if (p.toFile().exists()) {
return p.toRealPath().toString()
}
} while (searchDir = searchDir.getParent())
throw new GradleException("Could not find `react-native`");
}())
}
mavenCentral()
google()
Expand Down
32 changes: 20 additions & 12 deletions example/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,28 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryEr
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true

# Version of Flipper to use with React Native. Default value is whatever React
# Native defaults to. To disable Flipper, set it to `false`.
FLIPPER_VERSION=false

# Enable Fabric at runtime.
#USE_FABRIC=1

# Enable new architecture, i.e. Fabric + TurboModule - implies USE_FABRIC=1.
# Jetifier randomly fails on these libraries
android.jetifier.ignorelist=hermes-android

# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64

# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
# Note that this is incompatible with web debugging.
newArchEnabled=false
bridgelessEnabled=false

# Uncomment the line below to build React Native from source.
#react.buildFromSource=true

# Uncomment the line below if building react-native from source
#ANDROID_NDK_VERSION=21.4.7075529
# Version of Android NDK to build against.
#ANDROID_NDK_VERSION=26.1.10909125

# Version of Kotlin to build against.
#KOTLIN_VERSION=1.7.10
#KOTLIN_VERSION=1.8.22
Binary file modified example/android/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
3 changes: 2 additions & 1 deletion example/android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading

0 comments on commit a95955e

Please sign in to comment.