From 95c8807920110041f4b202eb8aea7d8a909bbb74 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Mon, 10 Oct 2022 02:51:06 -0700 Subject: [PATCH] Add sample component with state in RNTester (#34909) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/34909 This Diff introduces a Sample component in RNTester which uses the NativeState to load some images. It is an example on how to use CustomNativeState In this first diff, I focused on the iOS side of things. The next diff will make this work with Android. ## Changelog [iOS][Added] - Introduce sample component which work with the native state. Reviewed By: cortinico Differential Revision: D39884926 fbshipit-source-id: 9323d751fd06a1bb8ff93af836d97010c2095833 --- packages/rn-tester/BUCK | 41 +++++ .../NativeComponentWithState.podspec | 38 +++++ ...ponentWithStateCustomComponentDescriptor.h | 45 ++++++ ...tiveComponentWithStateCustomShadowNode.cpp | 61 ++++++++ ...NativeComponentWithStateCustomShadowNode.h | 60 ++++++++ .../ios/RNTNativeComponentWithStateView.h | 17 ++ .../ios/RNTNativeComponentWithStateView.mm | 145 ++++++++++++++++++ .../RNTNativeComponentWithStateViewManager.mm | 26 ++++ .../js/NativeComponentWithState.js | 32 ++++ packages/rn-tester/Podfile | 1 + packages/rn-tester/Podfile.lock | 2 +- packages/rn-tester/RNTester/AppDelegate.mm | 1 - .../NewArchitecture/ComponentWithState.js | 47 ++++++ .../rn-tester/js/utils/RNTesterList.ios.js | 6 + 14 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 packages/rn-tester/NativeComponentWithState/NativeComponentWithState.podspec create mode 100644 packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomComponentDescriptor.h create mode 100644 packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomShadowNode.cpp create mode 100644 packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomShadowNode.h create mode 100644 packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateView.h create mode 100644 packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateView.mm create mode 100644 packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateViewManager.mm create mode 100644 packages/rn-tester/NativeComponentWithState/js/NativeComponentWithState.js create mode 100644 packages/rn-tester/js/examples/NewArchitecture/ComponentWithState.js diff --git a/packages/rn-tester/BUCK b/packages/rn-tester/BUCK index 04c75fe503b596..49dfb98faa229b 100644 --- a/packages/rn-tester/BUCK +++ b/packages/rn-tester/BUCK @@ -51,6 +51,7 @@ rn_library( "js", "NativeModuleExample", "NativeComponentExample", + "NativeComponentWithState", "RCTTest", ], excludes = [ @@ -85,6 +86,7 @@ fb_native.filegroup( ], exclude = [ "NativeComponentExample/**/*", + "NativeComponentWithState/**/*", ], ), visibility = ["PUBLIC"], @@ -321,3 +323,42 @@ rn_xplat_cxx_library2( "//xplat/js/react-native-github:RCTFabricComponentViewsBase", ], ) + +rn_xplat_cxx_library2( + name = "NativeComponentWithState", + plugins_only = True, + srcs = glob( + [ + "NativeComponentWithState/ios/*.m", + "NativeComponentWithState/ios/*.mm", + "NativeComponentWithState/ios/*.cpp", + ], + ), + headers = glob( + [ + "NativeComponentWithState/ios/*.h", + ], + ), + header_namespace = "", + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++17", + "-Wall", + ], + contacts = ["oncall+react_native@xmail.facebook.com"], + labels = [ + "pfh:ReactNative_CommonInfrastructurePlaceholder", + "supermodule:xplat/default/public.react_native.infra", + ], + plugins = [ + react_fabric_component_plugin_provider("RNTNativeComponentWithStateView", "RNTNativeComponentWithStateCls"), + ], + plugins_header = "RCTFabricComponentsPlugins.h", + reexport_all_header_dependencies = False, + visibility = ["PUBLIC"], + deps = [ + ":generated_components-AppSpecs", + "//xplat/js/react-native-github:RCTFabricComponentViewsBase", + ], +) diff --git a/packages/rn-tester/NativeComponentWithState/NativeComponentWithState.podspec b/packages/rn-tester/NativeComponentWithState/NativeComponentWithState.podspec new file mode 100644 index 00000000000000..5412e48523f14a --- /dev/null +++ b/packages/rn-tester/NativeComponentWithState/NativeComponentWithState.podspec @@ -0,0 +1,38 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "../" "package.json"))) + +Pod::Spec.new do |s| + s.name = "NativeComponentWithState" + s.version = package["version"] + s.summary = package["description"] + s.description = "native-component-with-state" + s.homepage = "https://github.com/sota000/my-native-view.git" + s.license = "MIT" + s.platforms = { :ios => "12.4", :tvos => "12.4" } + s.compiler_flags = '-Wno-documentation -Wno-nullability-completeness' + s.author = "Facebook, Inc. and its affiliates" + s.source = { :git => "https://github.com/facebook/my-native-view.git", :tag => "#{s.version}" } + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/RCT-Folly\" \"$(PODS_ROOT)/boost\" \"${PODS_CONFIGURATION_BUILD_DIR}/React-Codegen/React_Codegen.framework/Headers\"", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + + s.source_files = "ios/**/*.{h,m,mm,cpp}" + s.requires_arc = true + + install_modules_dependencies(s) + + # Enable codegen for this library + use_react_native_codegen!(s, { + :library_name => "NativeComponentWithStateSpec", + :react_native_path => "../../../", + :js_srcs_dir => "./js", + :library_type => "components" + }) +end diff --git a/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomComponentDescriptor.h b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomComponentDescriptor.h new file mode 100644 index 00000000000000..988a1ac05966c9 --- /dev/null +++ b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomComponentDescriptor.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include "RNTNativeComponentWithStateCustomShadowNode.h" + +namespace facebook { +namespace react { + +/* + * Descriptor for + * component. + */ +class RNTNativeComponentWithStateCustomComponentDescriptor final + : public ConcreteComponentDescriptor< + RNTNativeComponentWithStateCustomShadowNode> { + public: + RNTNativeComponentWithStateCustomComponentDescriptor( + ComponentDescriptorParameters const ¶meters) + : ConcreteComponentDescriptor(parameters), + imageManager_(std::make_shared(contextContainer_)) {} + + void adopt(ShadowNode::Unshared const &shadowNode) const override { + ConcreteComponentDescriptor::adopt(shadowNode); + + auto compShadowNode = + std::static_pointer_cast( + shadowNode); + + // `RNTNativeComponentWithStateCustomShadowNode` uses `ImageManager` to + // initiate image loading and communicate the loading state + // and results to mounting layer. + compShadowNode->setImageManager(imageManager_); + } + + private: + const SharedImageManager imageManager_; +}; + +} // namespace react +} // namespace facebook diff --git a/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomShadowNode.cpp b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomShadowNode.cpp new file mode 100644 index 00000000000000..831eac6589a8dc --- /dev/null +++ b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomShadowNode.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "RNTNativeComponentWithStateCustomShadowNode.h" + +#include + +namespace facebook { +namespace react { + +extern const char RNTNativeComponentWithStateComponentName[] = + "RNTNativeComponentWithState"; + +void RNTNativeComponentWithStateCustomShadowNode::setImageManager( + const SharedImageManager &imageManager) { + ensureUnsealed(); + imageManager_ = imageManager; +} + +void RNTNativeComponentWithStateCustomShadowNode::updateStateIfNeeded() { + const auto &newImageSource = getImageSource(); + + auto const ¤tState = getStateData(); + + auto imageSource = currentState.getImageSource(); + + bool anyChanged = newImageSource != imageSource; + + if (!anyChanged) { + return; + } + + // Now we are about to mutate the Shadow Node. + ensureUnsealed(); + + // It is not possible to copy or move image requests from SliderLocalData, + // so instead we recreate any image requests (that may already be in-flight?) + // TODO: check if multiple requests are cached or if it's a net loss + auto state = RNTNativeComponentWithStateState{ + newImageSource, + imageManager_->requestImage(newImageSource, getSurfaceId())}; + setStateData(std::move(state)); +} + +ImageSource RNTNativeComponentWithStateCustomShadowNode::getImageSource() + const { + return getConcreteProps().imageSource; +} + +void RNTNativeComponentWithStateCustomShadowNode::layout( + LayoutContext layoutContext) { + updateStateIfNeeded(); + ConcreteViewShadowNode::layout(layoutContext); +} + +} // namespace react +} // namespace facebook diff --git a/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomShadowNode.h b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomShadowNode.h new file mode 100644 index 00000000000000..355733f3e9a245 --- /dev/null +++ b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateCustomShadowNode.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace facebook { +namespace react { + +JSI_EXPORT extern const char RNTNativeComponentWithStateComponentName[]; + +/* + * `ShadowNode` for component. + */ +class RNTNativeComponentWithStateCustomShadowNode final + : public ConcreteViewShadowNode< + RNTNativeComponentWithStateComponentName, + RNTNativeComponentWithStateProps, + RNTNativeComponentWithStateEventEmitter, + RNTNativeComponentWithStateState> { + public: + using ConcreteViewShadowNode::ConcreteViewShadowNode; + + // Associates a shared `ImageManager` with the node. + void setImageManager(const SharedImageManager &imageManager); + + static RNTNativeComponentWithStateState initialStateData( + ShadowNodeFragment const &fragment, + ShadowNodeFamilyFragment const &familyFragment, + ComponentDescriptor const &componentDescriptor) { + auto imageSource = ImageSource{ImageSource::Type::Invalid}; + return {imageSource, {imageSource, nullptr}}; + } + +#pragma mark - LayoutableShadowNode + + void layout(LayoutContext layoutContext) override; + + private: + void updateStateIfNeeded(); + + ImageSource getImageSource() const; + + SharedImageManager imageManager_; +}; + +} // namespace react +} // namespace facebook diff --git a/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateView.h b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateView.h new file mode 100644 index 00000000000000..7b736ea61f2f1f --- /dev/null +++ b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateView.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RNTNativeComponentWithStateView : RCTViewComponentView + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateView.mm b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateView.mm new file mode 100644 index 00000000000000..1af81940f8a49b --- /dev/null +++ b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateView.mm @@ -0,0 +1,145 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RNTNativeComponentWithStateView.h" + +#import +#import +#import +#import +#import "RNTNativeComponentWithStateCustomComponentDescriptor.h" + +#import +#import + +#import "RCTFabricComponentsPlugins.h" + +using namespace facebook::react; + +@interface RNTNativeComponentWithStateView () +@end + +@implementation RNTNativeComponentWithStateView { + UIView *_view; + UIImageView *_imageView; + UIImage *_image; + ImageResponseObserverCoordinator const *_imageCoordinator; + RCTImageResponseObserverProxy _imageResponseObserverProxy; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _view = [[UIView alloc] init]; + _view.backgroundColor = [UIColor redColor]; + + _imageView = [[UIImageView alloc] init]; + [_view addSubview:_imageView]; + + _imageView.translatesAutoresizingMaskIntoConstraints = NO; + [NSLayoutConstraint activateConstraints:@[ + [_imageView.topAnchor constraintEqualToAnchor:_view.topAnchor constant:10], + [_imageView.leftAnchor constraintEqualToAnchor:_view.leftAnchor constant:10], + [_imageView.bottomAnchor constraintEqualToAnchor:_view.bottomAnchor constant:-10], + [_imageView.rightAnchor constraintEqualToAnchor:_view.rightAnchor constant:-10], + ]]; + _imageView.image = _image; + + _imageResponseObserverProxy = RCTImageResponseObserverProxy(self); + + self.contentView = _view; + } + + return self; +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + self.imageCoordinator = nullptr; + _image = nil; +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + [super updateProps:props oldProps:oldProps]; +} + +- (void)updateState:(facebook::react::State::Shared const &)state + oldState:(facebook::react::State::Shared const &)oldState +{ + auto _state = std::static_pointer_cast(state); + auto _oldState = std::static_pointer_cast(oldState); + + auto data = _state->getData(); + + bool havePreviousData = _oldState != nullptr; + + auto getCoordinator = [](ImageRequest const *request) -> ImageResponseObserverCoordinator const * { + if (request) { + return &request->getObserverCoordinator(); + } else { + return nullptr; + } + }; + + if (!havePreviousData || data.getImageSource() != _oldState->getData().getImageSource()) { + self.imageCoordinator = getCoordinator(&data.getImageRequest()); + } +} + +- (void)setImageCoordinator:(const ImageResponseObserverCoordinator *)coordinator +{ + if (_imageCoordinator) { + _imageCoordinator->removeObserver(_imageResponseObserverProxy); + } + _imageCoordinator = coordinator; + if (_imageCoordinator) { + _imageCoordinator->addObserver(_imageResponseObserverProxy); + } +} + +- (void)setImage:(UIImage *)image +{ + if ([image isEqual:_image]) { + return; + } + + _imageView.image = image; +} + +#pragma mark - RCTImageResponseDelegate + +- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(void const *)observer +{ + if (observer == &_imageResponseObserverProxy) { + self.image = image; + } +} + +- (void)didReceiveProgress:(float)progress fromObserver:(void const *)observer +{ +} + +- (void)didReceiveFailureFromObserver:(void const *)observer +{ +} + +@end + +Class RNTNativeComponentWithStateCls(void) +{ + return RNTNativeComponentWithStateView.class; +} diff --git a/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateViewManager.mm b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateViewManager.mm new file mode 100644 index 00000000000000..4237e1538e7348 --- /dev/null +++ b/packages/rn-tester/NativeComponentWithState/ios/RNTNativeComponentWithStateViewManager.mm @@ -0,0 +1,26 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import + +@interface RNTNativeComponentWithStateViewManager : RCTViewManager +@end + +@implementation RNTNativeComponentWithStateViewManager + +RCT_EXPORT_MODULE(RNTNativeComponentWithState) + +RCT_EXPORT_VIEW_PROPERTY(imageSource, UIImage *) + +- (UIView *)view +{ + return [[UIView alloc] init]; +} + +@end diff --git a/packages/rn-tester/NativeComponentWithState/js/NativeComponentWithState.js b/packages/rn-tester/NativeComponentWithState/js/NativeComponentWithState.js new file mode 100644 index 00000000000000..4663185c6854d9 --- /dev/null +++ b/packages/rn-tester/NativeComponentWithState/js/NativeComponentWithState.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 + */ + +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +import type {ImageSource} from 'react-native/Libraries/Image/ImageSource'; +import type {HostComponent} from 'react-native/Libraries/Renderer/shims/ReactNativeTypes'; +import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; + +type NativeProps = $ReadOnly<{| + ...ViewProps, + imageSource: ImageSource, +|}>; + +// The NativeState is not used in JS-land, so we don't have to export it. +// eslint-disable-next-line no-unused-vars +type ComponentNativeState = $ReadOnly<{| + imageSource: ImageSource, + //$FlowFixMe[cannot-resolve-name]: this type is not exposed in JS but we can use it in the Native State. + imageRequest: ImageRequest, +|}>; + +export default (codegenNativeComponent( + 'RNTNativeComponentWithState', +): HostComponent); diff --git a/packages/rn-tester/Podfile b/packages/rn-tester/Podfile index 386a55c6e09131..1c9279c50c19ba 100644 --- a/packages/rn-tester/Podfile +++ b/packages/rn-tester/Podfile @@ -28,6 +28,7 @@ def pods(options = {}, use_flipper: !IN_CI && !USE_FRAMEWORKS) if ENV['RCT_NEW_ARCH_ENABLED'] == '1' # Custom fabric component is only supported when using codegen discovery. pod 'MyNativeView', :path => "NativeComponentExample" + pod 'NativeComponentWithState', :path => "NativeComponentWithState" end use_react_native!( diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index 564818c313b8c3..57c109f6770222 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -961,6 +961,6 @@ SPEC CHECKSUMS: Yoga: 1b1a12ff3d86a10565ea7cbe057d42f5e5fb2a07 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: e86c02825ce4e267e6fb3975bae791feb32a94a0 +PODFILE CHECKSUM: 6e26aac84dafab208188a8c6e45de8b3cf2a9f62 COCOAPODS: 1.11.3 diff --git a/packages/rn-tester/RNTester/AppDelegate.mm b/packages/rn-tester/RNTester/AppDelegate.mm index 2d1b676a5ec846..8fafba034302ba 100644 --- a/packages/rn-tester/RNTester/AppDelegate.mm +++ b/packages/rn-tester/RNTester/AppDelegate.mm @@ -63,7 +63,6 @@ #endif #import - #import "RNTesterTurboModuleProvider.h" @interface AppDelegate () { diff --git a/packages/rn-tester/js/examples/NewArchitecture/ComponentWithState.js b/packages/rn-tester/js/examples/NewArchitecture/ComponentWithState.js new file mode 100644 index 00000000000000..1194e4ec41a194 --- /dev/null +++ b/packages/rn-tester/js/examples/NewArchitecture/ComponentWithState.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 * as React from 'react'; +import {StyleSheet} from 'react-native'; +import NativeComponentWithState from '../../../NativeComponentWithState/js/NativeComponentWithState'; + +const styles = StyleSheet.create({ + component: { + marginLeft: 150, + width: 100, + height: 100, + marginTop: 20, + }, +}); + +exports.title = 'Component with State'; +exports.description = + 'Codegen discovery must be enabled for iOS. See Podfile for more details. Component with State'; +exports.examples = [ + { + title: 'Component with State', + description: + 'Change the image source in the Examples/NewArchitecture/ComponentWithState.js file', + render(): React.Element { + return ( + <> + + + ); + }, + }, +]; diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index cb0e34893de90b..79432b55382793 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -161,6 +161,12 @@ const Components: Array = [ module: require('../examples/NewArchitecture/NewArchitectureExample'), supportsTVOS: false, }, + { + key: 'ComponentWithState', + category: 'UI', + module: require('../examples/NewArchitecture/ComponentWithState'), + supportsTVOS: false, + }, ]; const APIs: Array = [