Skip to content
Permalink
Browse files
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
  • Loading branch information
hramos authored and facebook-github-bot committed Aug 31, 2019
1 parent 26a8d2e commit 63fa3f21c5ab308def450bffb22054241a8842ef
Showing 12 changed files with 381 additions and 4 deletions.
@@ -12,6 +12,7 @@
*/
#import "FBReactNativeSpec.h"

#import <folly/Optional.h>


namespace facebook {
@@ -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
{
@@ -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 {
@@ -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"];
@@ -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);
},
};
@@ -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);
@@ -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';
@@ -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');
},
@@ -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`)
@@ -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
@@ -33,6 +33,7 @@
#endif

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

@interface RCTUIManager (RCTRootView)

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

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

@@ -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];
@@ -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",
@@ -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);
@@ -18,6 +18,7 @@

static std::unordered_map<std::string, Class (*)(void)> sCoreModuleClassMap = {
{"AccessibilityManager", RCTAccessibilityManagerCls},
{"Appearance", RCTAppearanceCls},
{"DeviceInfo", RCTDeviceInfoCls},
{"ExceptionsManager", RCTExceptionsManagerCls},
{"ImageLoader", RCTImageLoaderCls},
@@ -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

0 comments on commit 63fa3f2

Please sign in to comment.