Skip to content

Commit

Permalink
Add Appearance native module
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hramos authored and facebook-github-bot committed Aug 31, 2019
1 parent 26a8d2e commit 63fa3f2
Show file tree
Hide file tree
Showing 12 changed files with 381 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
#import "FBReactNativeSpec.h"

#import <folly/Optional.h>


namespace facebook {
Expand Down Expand Up @@ -452,6 +453,69 @@ + (RCTManagedPointer *)JS_NativeAppState_SpecGetCurrentAppStateSuccessAppState:(

} // namespace react
} // namespace facebook
namespace facebook {
namespace react {


static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_getColorScheme(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, StringKind, "getColorScheme", @selector(getColorScheme), args, count);
}

static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_addListener(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "addListener", @selector(addListener:), args, count);
}

static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_removeListeners(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "removeListeners", @selector(removeListeners:), args, count);
}


NativeAppearanceSpecJSI::NativeAppearanceSpecJSI(id<RCTTurboModule> instance, std::shared_ptr<JSCallInvoker> jsInvoker)
: ObjCTurboModule("Appearance", instance, jsInvoker) {

methodMap_["getColorScheme"] = MethodMetadata {0, __hostFunction_NativeAppearanceSpecJSI_getColorScheme};


methodMap_["addListener"] = MethodMetadata {1, __hostFunction_NativeAppearanceSpecJSI_addListener};


methodMap_["removeListeners"] = MethodMetadata {1, __hostFunction_NativeAppearanceSpecJSI_removeListeners};



}

} // namespace react
} // namespace facebook
folly::Optional<NativeAppearanceColorSchemeName> NSStringToNativeAppearanceColorSchemeName(NSString *value) {
static NSDictionary *dict = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dict = @{
@"light": @0,
@"dark": @1,
};
});
return value ? (NativeAppearanceColorSchemeName)[dict[value] integerValue] : folly::Optional<NativeAppearanceColorSchemeName>{};
}

NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional<NativeAppearanceColorSchemeName> value) {
static NSDictionary *dict = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dict = @{
@0: @"light",
@1: @"dark",
};
});
return value.hasValue() ? dict[@(value.value())] : nil;
}
@implementation RCTCxxConvert (NativeAppearance_AppearancePreferences)
+ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json
{
return facebook::react::managedPointer<JS::NativeAppearance::AppearancePreferences>(json);
}
@end
@implementation RCTCxxConvert (NativeAsyncStorage_SpecMultiGetCallbackErrorsElement)
+ (RCTManagedPointer *)JS_NativeAsyncStorage_SpecMultiGetCallbackErrorsElement:(id)json
{
Expand Down
48 changes: 48 additions & 0 deletions Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,49 @@ namespace facebook {
};
} // namespace react
} // namespace facebook
@protocol NativeAppearanceSpec <RCTBridgeModule, RCTTurboModule>

- (NSString *)getColorScheme;
- (void)addListener:(NSString *)eventName;
- (void)removeListeners:(double)count;

@end
namespace facebook {
namespace react {
/**
* ObjC++ class for module 'Appearance'
*/

class JSI_EXPORT NativeAppearanceSpecJSI : public ObjCTurboModule {
public:
NativeAppearanceSpecJSI(id<RCTTurboModule> instance, std::shared_ptr<JSCallInvoker> jsInvoker);

};
} // namespace react
} // namespace facebook
typedef NS_ENUM(NSInteger, NativeAppearanceColorSchemeName) {
NativeAppearanceColorSchemeNameLight = 0,
NativeAppearanceColorSchemeNameDark,
};

folly::Optional<NativeAppearanceColorSchemeName> NSStringToNativeAppearanceColorSchemeName(NSString *value);
NSString *NativeAppearanceColorSchemeNameToNSString(folly::Optional<NativeAppearanceColorSchemeName> value);

namespace JS {
namespace NativeAppearance {
struct AppearancePreferences {
NSString *colorScheme() const;

AppearancePreferences(NSDictionary *const v) : _v(v) {}
private:
NSDictionary *_v;
};
}
}

@interface RCTCxxConvert (NativeAppearance_AppearancePreferences)
+ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json;
@end

namespace JS {
namespace NativeAsyncStorage {
Expand Down Expand Up @@ -2553,6 +2596,11 @@ inline JS::NativeAppState::Constants::Builder::Builder(const Input i) : _factory
inline JS::NativeAppState::Constants::Builder::Builder(Constants i) : _factory(^{
return i.unsafeRawValue();
}) {}
inline NSString *JS::NativeAppearance::AppearancePreferences::colorScheme() const
{
id const p = _v[@"colorScheme"];
return RCTBridgingToString(p);
}
inline NSString *JS::NativeAsyncStorage::SpecMultiGetCallbackErrorsElement::message() const
{
id const p = _v[@"message"];
Expand Down
79 changes: 79 additions & 0 deletions Libraries/Utilities/Appearance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/

'use strict';

import EventEmitter from '../vendor/emitter/EventEmitter';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import NativeAppearance, {
type AppearancePreferences,
type ColorSchemeName,
} from './NativeAppearance';
import invariant from 'invariant';

type AppearanceListener = (preferences: AppearancePreferences) => void;
const eventEmitter = new EventEmitter();

const nativeColorScheme: ?string =
NativeAppearance == null ? null : NativeAppearance.getColorScheme();
invariant(
nativeColorScheme === 'dark' ||
nativeColorScheme === 'light' ||
nativeColorScheme == null,
"Unrecognized color scheme. Did you mean 'dark' or 'light'?",
);

let currentColorScheme: ?ColorSchemeName = nativeColorScheme;

if (NativeAppearance) {
const nativeEventEmitter = new NativeEventEmitter(NativeAppearance);
nativeEventEmitter.addListener(
'appearanceChanged',
(newAppearance: AppearancePreferences) => {
const {colorScheme} = newAppearance;
invariant(
colorScheme === 'dark' ||
colorScheme === 'light' ||
colorScheme == null,
"Unrecognized color scheme. Did you mean 'dark' or 'light'?",
);
currentColorScheme = colorScheme;
eventEmitter.emit('change', {colorScheme});
},
);
}

module.exports = {
/**
* Note: Although color scheme is available immediately, it may change at any
* time. Any rendering logic or styles that depend on this should try to call
* this function on every render, rather than caching the value (for example,
* using inline styles rather than setting a value in a `StyleSheet`).
*
* Example: `const colorScheme = Appearance.getColorScheme();`
*
* @returns {?ColorSchemeName} Value for the color scheme preference.
*/
getColorScheme(): ?ColorSchemeName {
return currentColorScheme;
},
/**
* Add an event handler that is fired when appearance preferences change.
*/
addChangeListener(listener: AppearanceListener): void {
eventEmitter.addListener('change', listener);
},
/**
* Remove an event handler.
*/
removeChangeListener(listener: AppearanceListener): void {
eventEmitter.removeListener('change', listener);
},
};
36 changes: 36 additions & 0 deletions Libraries/Utilities/NativeAppearance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

'use strict';

import type {TurboModule} from '../TurboModule/RCTExport';
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';

export type ColorSchemeName = 'light' | 'dark';

export type AppearancePreferences = {|
// TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
// types.
/* 'light' | 'dark' */
colorScheme?: ?string,
|};

export interface Spec extends TurboModule {
// TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union
// types.
/* 'light' | 'dark' */
+getColorScheme: () => ?string;

// RCTEventEmitter
+addListener: (eventName: string) => void;
+removeListeners: (count: number) => void;
}

export default (TurboModuleRegistry.get<Spec>('Appearance'): ?Spec);
4 changes: 4 additions & 0 deletions Libraries/react-native/react-native-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import typeof VirtualizedSectionList from '../Lists/VirtualizedSectionList';
import typeof ActionSheetIOS from '../ActionSheetIOS/ActionSheetIOS';
import typeof Alert from '../Alert/Alert';
import typeof Animated from '../Animated/src/Animated';
import typeof Appearance from '../Utilities/Appearance';
import typeof AppRegistry from '../ReactNative/AppRegistry';
import typeof AppState from '../AppState/AppState';
import typeof AsyncStorage from '../Storage/AsyncStorage';
Expand Down Expand Up @@ -252,6 +253,9 @@ module.exports = {
get Animated(): Animated {
return require('../Animated/src/Animated');
},
get Appearance(): Appearance {
return require('../Utilities/Appearance');
},
get AppRegistry(): AppRegistry {
return require('../ReactNative/AppRegistry');
},
Expand Down
6 changes: 3 additions & 3 deletions RNTester/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ PODS:
- React-jsi (= 1000.0.0)
- ReactCommon/jscallinvoker (= 1000.0.0)
- ReactCommon/turbomodule/core (= 1000.0.0)
- Yoga (1000.0.0.React)
- Yoga (1.14.0)

DEPENDENCIES:
- DoubleConversion (from `../third-party-podspecs/DoubleConversion.podspec`)
Expand Down Expand Up @@ -375,8 +375,8 @@ SPEC CHECKSUMS:
React-RCTText: 9078167d3bc011162326f2d8ef4dd580ec1eca17
React-RCTVibration: 63c20d89204937ff8c7bbc1e712383347e6fbd90
ReactCommon: 63d1a6355d5810a21a61efda9ac93804571a1b8b
Yoga: d2044f32d047e7f5a36b6894347569f069c0f9b7
Yoga: 0abc4039ca4c0de783ab88c0ee21273583cbc2af

PODFILE CHECKSUM: 060903e270072f1e192b064848e6c34528af1c87

COCOAPODS: 1.7.2
COCOAPODS: 1.7.1
19 changes: 18 additions & 1 deletion React/Base/RCTRootView.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#endif

NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification";
NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification";

@interface RCTUIManager (RCTRootView)

Expand Down Expand Up @@ -347,7 +348,7 @@ - (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize
if (bothSizesHaveAZeroDimension || sizesAreEqual) {
return;
}

[self invalidateIntrinsicContentSize];
[self.superview setNeedsLayout];

Expand All @@ -366,6 +367,22 @@ - (void)contentViewInvalidated
[self showLoadingView];
}

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];

if (@available(iOS 13.0, *)) {
if ([previousTraitCollection hasDifferentColorAppearanceComparedToTraitCollection:self.traitCollection]) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTUserInterfaceStyleDidChangeNotification
object:self
userInfo:@{@"traitCollection": self.traitCollection}];
}
}
}
#endif

- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
Expand Down
4 changes: 4 additions & 0 deletions React/CoreModules/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ rn_apple_library(
name = "AccessibilityManager",
native_class_func = "RCTAccessibilityManagerCls",
) +
react_module_plugin_providers(
name = "Appearance",
native_class_func = "RCTAppearanceCls",
) +
react_module_plugin_providers(
name = "DeviceInfo",
native_class_func = "RCTDeviceInfoCls",
Expand Down
1 change: 1 addition & 0 deletions React/CoreModules/CoreModulesPlugins.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Class RCTCoreModulesClassProvider(const char *name);

// Lookup functions
Class RCTAccessibilityManagerCls(void);
Class RCTAppearanceCls(void);
Class RCTDeviceInfoCls(void);
Class RCTExceptionsManagerCls(void);
Class RCTImageLoaderCls(void);
Expand Down
1 change: 1 addition & 0 deletions React/CoreModules/CoreModulesPlugins.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

static std::unordered_map<std::string, Class (*)(void)> sCoreModuleClassMap = {
{"AccessibilityManager", RCTAccessibilityManagerCls},
{"Appearance", RCTAppearanceCls},
{"DeviceInfo", RCTDeviceInfoCls},
{"ExceptionsManager", RCTExceptionsManagerCls},
{"ImageLoader", RCTImageLoaderCls},
Expand Down
16 changes: 16 additions & 0 deletions React/CoreModules/RCTAppearance.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <UIKit/UIKit.h>

#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

NSString *const RCTUserInterfaceStyleDidChangeNotification = @"RCTUserInterfaceStyleDidChangeNotification";

@interface RCTAppearance : RCTEventEmitter <RCTBridgeModule>
@end
Loading

0 comments on commit 63fa3f2

Please sign in to comment.