Skip to content

Commit 63fa3f2

Browse files
hramosfacebook-github-bot
authored andcommitted
Add Appearance native module
Summary: Implements the Appearance native module as discussed in react-native-community/discussions-and-proposals#126. The purpose of the Appearance native module is to expose the user's appearance preferences. It provides a basic get() API that returns the user's preferred color scheme on iOS 13 devices, also known as Dark Mode. It also provides the ability to subscribe to events whenever an appearance preference changes. The name, "Appearance", was chosen purposefully to allow for future expansion to cover other appearance preferences such as reduced motion, reduced transparency, or high contrast modes. Changelog: [iOS] [Added] - The Appearance native module can be used to prepare your app for Dark Mode on iOS 13. Reviewed By: yungsters Differential Revision: D16699954 fbshipit-source-id: 03b4cc5d2a1a69f31f3a6d9bece23f6867b774ea
1 parent 26a8d2e commit 63fa3f2

File tree

12 files changed

+381
-4
lines changed

12 files changed

+381
-4
lines changed

Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm

+64
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
#import "FBReactNativeSpec.h"
1414

15+
#import <folly/Optional.h>
1516

1617

1718
namespace facebook {
@@ -452,6 +453,69 @@ + (RCTManagedPointer *)JS_NativeAppState_SpecGetCurrentAppStateSuccessAppState:(
452453

453454
} // namespace react
454455
} // namespace facebook
456+
namespace facebook {
457+
namespace react {
458+
459+
460+
static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_getColorScheme(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
461+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, StringKind, "getColorScheme", @selector(getColorScheme), args, count);
462+
}
463+
464+
static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_addListener(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
465+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "addListener", @selector(addListener:), args, count);
466+
}
467+
468+
static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_removeListeners(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
469+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "removeListeners", @selector(removeListeners:), args, count);
470+
}
471+
472+
473+
NativeAppearanceSpecJSI::NativeAppearanceSpecJSI(id<RCTTurboModule> instance, std::shared_ptr<JSCallInvoker> jsInvoker)
474+
: ObjCTurboModule("Appearance", instance, jsInvoker) {
475+
476+
methodMap_["getColorScheme"] = MethodMetadata {0, __hostFunction_NativeAppearanceSpecJSI_getColorScheme};
477+
478+
479+
methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeAppearanceSpecJSI_addListener};
480+
481+
482+
methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeAppearanceSpecJSI_removeListeners};
483+
484+
485+
486+
}
487+
488+
} // namespace react
489+
} // namespace facebook
490+
folly::Optional<NativeAppearanceColorSchemeName> NSStringToNativeAppearanceColorSchemeName(NSString *value) {
491+
static NSDictionary *dict = nil;
492+
static dispatch_once_t onceToken;
493+
dispatch_once(&onceToken, ^{
494+
dict = @{
495+
@"light": @0,
496+
@"dark": @1,
497+
};
498+
});
499+
return value ? (NativeAppearanceColorSchemeName)[dict[value] integerValue] : folly::Optional<NativeAppearanceColorSchemeName>{};
500+
}
501+
502+
NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional<NativeAppearanceColorSchemeName> value) {
503+
static NSDictionary *dict = nil;
504+
static dispatch_once_t onceToken;
505+
dispatch_once(&onceToken, ^{
506+
dict = @{
507+
@0: @"light",
508+
@1: @"dark",
509+
};
510+
});
511+
return value.hasValue() ? dict[@(value.value())] : nil;
512+
}
513+
@implementation RCTCxxConvert (NativeAppearance_AppearancePreferences)
514+
+ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json
515+
{
516+
return facebook::react::managedPointer<JS::NativeAppearance::AppearancePreferences>(json);
517+
}
518+
@end
455519
@implementation RCTCxxConvert (NativeAsyncStorage_SpecMultiGetCallbackErrorsElement)
456520
+ (RCTManagedPointer *)JS_NativeAsyncStorage_SpecMultiGetCallbackErrorsElement:(id)json
457521
{

Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h

+48
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,49 @@ namespace facebook {
432432
};
433433
} // namespace react
434434
} // namespace facebook
435+
@protocol NativeAppearanceSpec <RCTBridgeModule, RCTTurboModule>
436+
437+
- (NSString *)getColorScheme;
438+
- (void)addListener:(NSString *)eventName;
439+
- (void)removeListeners:(double)count;
440+
441+
@end
442+
namespace facebook {
443+
namespace react {
444+
/**
445+
* ObjC++ class for module 'Appearance'
446+
*/
447+
448+
class JSI_EXPORT NativeAppearanceSpecJSI : public ObjCTurboModule {
449+
public:
450+
NativeAppearanceSpecJSI(id<RCTTurboModule> instance, std::shared_ptr<JSCallInvoker> jsInvoker);
451+
452+
};
453+
} // namespace react
454+
} // namespace facebook
455+
typedef NS_ENUM(NSInteger, NativeAppearanceColorSchemeName) {
456+
NativeAppearanceColorSchemeNameLight = 0,
457+
NativeAppearanceColorSchemeNameDark,
458+
};
459+
460+
folly::Optional<NativeAppearanceColorSchemeName> NSStringToNativeAppearanceColorSchemeName(NSString *value);
461+
NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional<NativeAppearanceColorSchemeName> value);
462+
463+
namespace JS {
464+
namespace NativeAppearance {
465+
struct AppearancePreferences {
466+
NSString *colorScheme() const;
467+
468+
AppearancePreferences(NSDictionary *const v) : _v(v) {}
469+
private:
470+
NSDictionary *_v;
471+
};
472+
}
473+
}
474+
475+
@interface RCTCxxConvert (NativeAppearance_AppearancePreferences)
476+
+ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json;
477+
@end
435478

436479
namespace JS {
437480
namespace NativeAsyncStorage {
@@ -2553,6 +2596,11 @@ inline JS::NativeAppState::Constants::Builder::Builder(const Input i) : _factory
25532596
inline JS::NativeAppState::Constants::Builder::Builder(Constants i) : _factory(^{
25542597
return i.unsafeRawValue();
25552598
}) {}
2599+
inline NSString *JS::NativeAppearance::AppearancePreferences::colorScheme() const
2600+
{
2601+
id const p = _v[@"colorScheme"];
2602+
return RCTBridgingToString(p);
2603+
}
25562604
inline NSString *JS::NativeAsyncStorage::SpecMultiGetCallbackErrorsElement::message() const
25572605
{
25582606
id const p = _v[@"message"];

Libraries/Utilities/Appearance.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow
9+
*/
10+
11+
'use strict';
12+
13+
import EventEmitter from '../vendor/emitter/EventEmitter';
14+
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
15+
import NativeAppearance, {
16+
type AppearancePreferences,
17+
type ColorSchemeName,
18+
} from './NativeAppearance';
19+
import invariant from 'invariant';
20+
21+
type AppearanceListener = (preferences: AppearancePreferences) => void;
22+
const eventEmitter = new EventEmitter();
23+
24+
const nativeColorScheme: ?string =
25+
NativeAppearance == null ? null : NativeAppearance.getColorScheme();
26+
invariant(
27+
nativeColorScheme === 'dark' ||
28+
nativeColorScheme === 'light' ||
29+
nativeColorScheme == null,
30+
"Unrecognized color scheme. Did you mean 'dark' or 'light'?",
31+
);
32+
33+
let currentColorScheme: ?ColorSchemeName = nativeColorScheme;
34+
35+
if (NativeAppearance) {
36+
const nativeEventEmitter = new NativeEventEmitter(NativeAppearance);
37+
nativeEventEmitter.addListener(
38+
'appearanceChanged',
39+
(newAppearance: AppearancePreferences) => {
40+
const {colorScheme} = newAppearance;
41+
invariant(
42+
colorScheme === 'dark' ||
43+
colorScheme === 'light' ||
44+
colorScheme == null,
45+
"Unrecognized color scheme. Did you mean 'dark' or 'light'?",
46+
);
47+
currentColorScheme = colorScheme;
48+
eventEmitter.emit('change', {colorScheme});
49+
},
50+
);
51+
}
52+
53+
module.exports = {
54+
/**
55+
* Note: Although color scheme is available immediately, it may change at any
56+
* time. Any rendering logic or styles that depend on this should try to call
57+
* this function on every render, rather than caching the value (for example,
58+
* using inline styles rather than setting a value in a `StyleSheet`).
59+
*
60+
* Example: `const colorScheme = Appearance.getColorScheme();`
61+
*
62+
* @returns {?ColorSchemeName} Value for the color scheme preference.
63+
*/
64+
getColorScheme(): ?ColorSchemeName {
65+
return currentColorScheme;
66+
},
67+
/**
68+
* Add an event handler that is fired when appearance preferences change.
69+
*/
70+
addChangeListener(listener: AppearanceListener): void {
71+
eventEmitter.addListener('change', listener);
72+
},
73+
/**
74+
* Remove an event handler.
75+
*/
76+
removeChangeListener(listener: AppearanceListener): void {
77+
eventEmitter.removeListener('change', listener);
78+
},
79+
};
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
import type {TurboModule} from '../TurboModule/RCTExport';
14+
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
15+
16+
export type ColorSchemeName = 'light' | 'dark';
17+
18+
export type AppearancePreferences = {|
19+
// TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
20+
// types.
21+
/* 'light' | 'dark' */
22+
colorScheme?: ?string,
23+
|};
24+
25+
export interface Spec extends TurboModule {
26+
// TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
27+
// types.
28+
/* 'light' | 'dark' */
29+
+getColorScheme: () => ?string;
30+
31+
// RCTEventEmitter
32+
+addListener: (eventName: string) => void;
33+
+removeListeners: (count: number) => void;
34+
}
35+
36+
export default (TurboModuleRegistry.get<Spec>('Appearance'): ?Spec);

Libraries/react-native/react-native-implementation.js

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import typeof VirtualizedSectionList from '../Lists/VirtualizedSectionList';
4949
import typeof ActionSheetIOS from '../ActionSheetIOS/ActionSheetIOS';
5050
import typeof Alert from '../Alert/Alert';
5151
import typeof Animated from '../Animated/src/Animated';
52+
import typeof Appearance from '../Utilities/Appearance';
5253
import typeof AppRegistry from '../ReactNative/AppRegistry';
5354
import typeof AppState from '../AppState/AppState';
5455
import typeof AsyncStorage from '../Storage/AsyncStorage';
@@ -252,6 +253,9 @@ module.exports = {
252253
get Animated(): Animated {
253254
return require('../Animated/src/Animated');
254255
},
256+
get Appearance(): Appearance {
257+
return require('../Utilities/Appearance');
258+
},
255259
get AppRegistry(): AppRegistry {
256260
return require('../ReactNative/AppRegistry');
257261
},

RNTester/Podfile.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ PODS:
248248
- React-jsi (= 1000.0.0)
249249
- ReactCommon/jscallinvoker (= 1000.0.0)
250250
- ReactCommon/turbomodule/core (= 1000.0.0)
251-
- Yoga (1000.0.0.React)
251+
- Yoga (1.14.0)
252252

253253
DEPENDENCIES:
254254
- DoubleConversion (from `../third-party-podspecs/DoubleConversion.podspec`)
@@ -375,8 +375,8 @@ SPEC CHECKSUMS:
375375
React-RCTText: 9078167d3bc011162326f2d8ef4dd580ec1eca17
376376
React-RCTVibration: 63c20d89204937ff8c7bbc1e712383347e6fbd90
377377
ReactCommon: 63d1a6355d5810a21a61efda9ac93804571a1b8b
378-
Yoga: d2044f32d047e7f5a36b6894347569f069c0f9b7
378+
Yoga: 0abc4039ca4c0de783ab88c0ee21273583cbc2af
379379

380380
PODFILE CHECKSUM: 060903e270072f1e192b064848e6c34528af1c87
381381

382-
COCOAPODS: 1.7.2
382+
COCOAPODS: 1.7.1

React/Base/RCTRootView.m

+18-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#endif
3434

3535
NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification";
36+
NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification";
3637

3738
@interface RCTUIManager (RCTRootView)
3839

@@ -347,7 +348,7 @@ - (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize
347348
if (bothSizesHaveAZeroDimension || sizesAreEqual) {
348349
return;
349350
}
350-
351+
351352
[self invalidateIntrinsicContentSize];
352353
[self.superview setNeedsLayout];
353354

@@ -366,6 +367,22 @@ - (void)contentViewInvalidated
366367
[self showLoadingView];
367368
}
368369

370+
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
371+
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
372+
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
373+
{
374+
[super traitCollectionDidChange:previousTraitCollection];
375+
376+
if (@available(iOS 13.0, *)) {
377+
if ([previousTraitCollection hasDifferentColorAppearanceComparedToTraitCollection:self.traitCollection]) {
378+
[[NSNotificationCenter defaultCenter] postNotificationName:RCTUserInterfaceStyleDidChangeNotification
379+
object:self
380+
userInfo:@{@"traitCollection": self.traitCollection}];
381+
}
382+
}
383+
}
384+
#endif
385+
369386
- (void)dealloc
370387
{
371388
[[NSNotificationCenter defaultCenter] removeObserver:self];

React/CoreModules/BUCK

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ rn_apple_library(
4141
name = "AccessibilityManager",
4242
native_class_func = "RCTAccessibilityManagerCls",
4343
) +
44+
react_module_plugin_providers(
45+
name = "Appearance",
46+
native_class_func = "RCTAppearanceCls",
47+
) +
4448
react_module_plugin_providers(
4549
name = "DeviceInfo",
4650
native_class_func = "RCTDeviceInfoCls",

React/CoreModules/CoreModulesPlugins.h

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Class RCTCoreModulesClassProvider(const char *name);
3030

3131
// Lookup functions
3232
Class RCTAccessibilityManagerCls(void);
33+
Class RCTAppearanceCls(void);
3334
Class RCTDeviceInfoCls(void);
3435
Class RCTExceptionsManagerCls(void);
3536
Class RCTImageLoaderCls(void);

React/CoreModules/CoreModulesPlugins.mm

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
static std::unordered_map<std::string, Class (*)(void)> sCoreModuleClassMap = {
2020
{"AccessibilityManager", RCTAccessibilityManagerCls},
21+
{"Appearance", RCTAppearanceCls},
2122
{"DeviceInfo", RCTDeviceInfoCls},
2223
{"ExceptionsManager", RCTExceptionsManagerCls},
2324
{"ImageLoader", RCTImageLoaderCls},

React/CoreModules/RCTAppearance.h

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
#import <React/RCTBridgeModule.h>
11+
#import <React/RCTEventEmitter.h>
12+
13+
NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification";
14+
15+
@interface RCTAppearance : RCTEventEmitter <RCTBridgeModule>
16+
@end

0 commit comments

Comments
 (0)