Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS: Add Appearance TurboModule, useColorScheme hook [DO NOT MERGE] #26143

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,46 @@ + (RCTManagedPointer *)JS_NativeAppState_SpecGetCurrentAppStateSuccessAppState:(



}

} // namespace react
} // namespace facebook
@implementation RCTCxxConvert (NativeAppearance_AppearancePreferences)
+ (RCTManagedPointer *)JS_NativeAppearance_AppearancePreferences:(id)json
{
return facebook::react::managedPointer<JS::NativeAppearance::AppearancePreferences>(json);
}
@end
namespace facebook {
namespace react {


static facebook::jsi::Value __hostFunction_NativeAppearanceSpecJSI_getPreferences(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, ObjectKind, "getPreferences", @selector(getPreferences), 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_["getPreferences"] = MethodMetadata {0, __hostFunction_NativeAppearanceSpecJSI_getPreferences};


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


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



}

} // namespace react
Expand Down
41 changes: 41 additions & 0 deletions Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,42 @@ namespace facebook {
} // namespace react
} // namespace facebook

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
@protocol NativeAppearanceSpec <RCTBridgeModule, RCTTurboModule>

- (NSDictionary *)getPreferences;
- (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

namespace JS {
namespace NativeAsyncStorage {
struct SpecMultiGetCallbackErrorsElement {
Expand Down Expand Up @@ -2553,6 +2589,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
104 changes: 104 additions & 0 deletions Libraries/Utilities/Appearance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* 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} from './NativeAppearance';
import invariant from 'invariant';

const COLOR_SCHEME_NAME = {
light: 'light',
dark: 'dark',
noPreference: 'no-preference',
};
type ColorSchemeName = $Keys<{
light: string,
dark: string,
noPreference: string,
}>;
type AppearanceListener = (preferences: AppearancePreferences) => void;

const eventEmitter = new EventEmitter();

let appearancePreferencesInitialized = false;
let appearancePreferences: AppearancePreferences;

class Appearance {
/**
* Note: Although appearance is available immediately, it may change (e.g
* Dark Mode) so 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.get('colorScheme');`
*
* @param {string} preference Name of preference (e.g. 'colorScheme').
* @returns {ColorSchemeName} Value for the preference.
*/
static get(preference: string): ColorSchemeName {
invariant(
appearancePreferences[preference],
'No preference set for key ' + preference,
);
return appearancePreferences[preference];
}

/**
* This should only be called from native code by sending the
* appearanceChanged event.
*
* @param {object} appearancePreferences Simple string-keyed object of
* appearance preferences to set.
*/
static set(preferences: AppearancePreferences): void {
let {colorScheme} = preferences;
appearancePreferences = {colorScheme};

if (appearancePreferencesInitialized) {
// Don't fire 'change' the first time the dimensions are set.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/dimensions/appearance preferences/

eventEmitter.emit('change', preferences);
} else {
appearancePreferencesInitialized = true;
}
}

/**
* Add an event handler that is fired when appearance preferences change.
*/
static addChangeListener(listener: AppearanceListener): void {
eventEmitter.addListener('change', listener);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should return the result of this, an EventSubscription (which people can just call .remove() on) rather than depend on removeChangeListener

}

/**
* Remove an event handler.
*/
static removeChangeListener(listener: AppearanceListener): void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should get rid of this function

eventEmitter.removeListener('change', listener);
}
}

if (NativeAppearance) {
const nativeEventEmitter = new NativeEventEmitter(NativeAppearance);
// Subscribe before calling to make sure we don't miss any updates in between.
nativeEventEmitter.addListener(
'appearanceChanged',
(newAppearance: AppearancePreferences) => {
Appearance.set(newAppearance);
},
);
Appearance.set(NativeAppearance.getPreferences());
} else {
Appearance.set({colorScheme: 'no-preference'});
}

module.exports = Appearance;
28 changes: 28 additions & 0 deletions Libraries/Utilities/NativeAppearance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 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
* @format
*/

'use strict';

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

export type AppearancePreferences = {|
colorScheme?: string,
|};

export interface Spec extends TurboModule {
+getPreferences: () => AppearancePreferences;

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

export default (TurboModuleRegistry.get<Spec>('Appearance'): ?Spec);
30 changes: 30 additions & 0 deletions Libraries/Utilities/useColorScheme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* 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 {useMemo} from 'react';
import {useSubscription} from 'use-subscription';
import Appearance from './Appearance';

export default function useColorScheme() {
const subscription = useMemo(
() => ({
getCurrentValue: () => Appearance.get('colorScheme'),
subscribe: callback => {
Appearance.addChangeListener(callback);
return () => Appearance.removeChangeListener(callback);
},
}),
[],
);

return useSubscription(subscription);
}
6 changes: 6 additions & 0 deletions Libraries/react-native/react-native-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ module.exports = {
get Animated() {
return require('../Animated/src/Animated');
},
get Appearance() {
return require('../Utilities/Appearance');
},
get AppRegistry() {
return require('../ReactNative/AppRegistry');
},
Expand Down Expand Up @@ -299,6 +302,9 @@ module.exports = {
get unstable_batchedUpdates() {
return require('../Renderer/shims/ReactNative').unstable_batchedUpdates;
},
get useColorScheme() {
return require('../Utilities/useColorScheme').default;
},
get useWindowDimensions() {
return require('../Utilities/useWindowDimensions').default;
},
Expand Down
Loading