diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index 8a358806be1b62..9f36cd398282b2 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -424,6 +424,7 @@ let reactCore = RNTarget( let reactFabric = RNTarget( name: .reactFabric, path: "ReactCommon/react/renderer", + searchPaths: ["ReactCommon/react/renderer/imagemanager/platform/ios"], excludedPaths: [ "animations/tests", "attributedstring/tests", diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h new file mode 100644 index 00000000000000..ec99ac1b5cd3ae --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h @@ -0,0 +1,30 @@ +/* + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@protocol RCTBackgroundImageURLLoaderDelegate + +- (void)backgroundImagesDidLoad; + +@end + +@interface RCTBackgroundImageURLLoader : NSObject + +@property (nonatomic, weak) id delegate; + +- (void)updateStateWithNewState:(facebook::react::ViewShadowNode::ConcreteState::Shared)state oldState:(facebook::react::ViewShadowNode::ConcreteState::Shared)oldState; +- (nullable UIImage *)loadedImageForUri:(NSString *)uri; +- (void)reset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm new file mode 100644 index 00000000000000..c604869a52a206 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm @@ -0,0 +1,147 @@ +/* + * 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 "RCTBackgroundImageURLLoader.h" + +#import +#import +#import + +#include +#include + +using namespace facebook::react; + +@implementation RCTBackgroundImageURLLoader { + ViewShadowNode::ConcreteState::Shared _state; + std::map _uriToObserver; + NSMutableDictionary *_loadedImages; + NSMutableSet *_completedUris; +} + +- (instancetype)init +{ + if (self = [super init]) { + _loadedImages = [NSMutableDictionary new]; + _completedUris = [NSMutableSet new]; + } + return self; +} + +- (void)updateStateWithNewState:(ViewShadowNode::ConcreteState::Shared)state + oldState:(ViewShadowNode::ConcreteState::Shared)oldState +{ + const auto* oldRequests = oldState ? &oldState->getData().getBackgroundImageRequests() : nullptr; + const auto* newRequests = state ? &state->getData().getBackgroundImageRequests() : nullptr; + + if (oldRequests && newRequests && *oldRequests == *newRequests) { + return; + } + + if (oldRequests) { + for (const auto& request : *oldRequests) { + if (request.imageRequest) { + auto it = _uriToObserver.find(request.imageSource.uri); + if (it != _uriToObserver.end()) { + auto& observerCoordinator = request.imageRequest->getObserverCoordinator(); + observerCoordinator.removeObserver(it->second); + } + } + } + } + + _state = state; + _uriToObserver.clear(); + [_loadedImages removeAllObjects]; + [_completedUris removeAllObjects]; + + if (newRequests) { + for (const auto &request : *newRequests) { + if (request.imageRequest) { + const std::string &uri = request.imageSource.uri; + auto [it, inserted] = _uriToObserver.emplace(uri, self); + if (inserted) { + auto& observerCoordinator = request.imageRequest->getObserverCoordinator(); + observerCoordinator.addObserver(it->second); + } + } + } + } +} + +- (UIImage *)loadedImageForUri:(NSString *)uri +{ + return _loadedImages[uri]; +} + +- (void)reset +{ + if (_state) { + const auto &requests = _state->getData().getBackgroundImageRequests(); + for (const auto &request : requests) { + if (request.imageRequest) { + auto it = _uriToObserver.find(request.imageSource.uri); + if (it != _uriToObserver.end()) { + auto& observerCoordinator = request.imageRequest->getObserverCoordinator(); + observerCoordinator.removeObserver(it->second); + } + } + } + } + + _state = nullptr; + _uriToObserver.clear(); + [_loadedImages removeAllObjects]; + [_completedUris removeAllObjects]; +} + +#pragma mark - RCTImageResponseDelegate + +- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer +{ + for (const auto& [uri, observerProxy] : _uriToObserver) { + if (&observerProxy == observer) { + NSString *nsUri = [NSString stringWithUTF8String:uri.c_str()]; + _loadedImages[nsUri] = image; + [_completedUris addObject:nsUri]; + break; + } + } + + [self notifyDelegateIfAllImagesLoaded]; +} + +- (void)didReceiveProgress:(float)progress + loaded:(int64_t)loaded + total:(int64_t)total + fromObserver:(const void *)observer +{ + // Progress tracking not needed for background images +} + +- (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer +{ + for (const auto& [uri, observerProxy] : _uriToObserver) { + if (&observerProxy == observer) { + NSString *nsUri = [NSString stringWithUTF8String:uri.c_str()]; + RCTLogWarn(@"Failed to load background image: %@ - %@", nsUri, error); + [_completedUris addObject:nsUri]; + break; + } + } + + [self notifyDelegateIfAllImagesLoaded]; +} + +- (void)notifyDelegateIfAllImagesLoaded +{ + if (_completedUris.count == _uriToObserver.size()) { + [_delegate backgroundImagesDidLoad]; + } +} + +@end diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index a3c58cb16f577e..51289485561e8f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -17,12 +17,14 @@ #import #import +#import "RCTBackgroundImageURLLoader.h" + NS_ASSUME_NONNULL_BEGIN /** * UIView class for component. */ -@interface RCTViewComponentView : UIView { +@interface RCTViewComponentView : UIView { @protected facebook::react::LayoutMetrics _layoutMetrics; facebook::react::SharedViewProps _props; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 7ed3baaf122b61..9ac33bdc31720d 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -53,6 +53,7 @@ @implementation RCTViewComponentView { BOOL _useCustomContainerView; NSMutableSet *_accessibilityOrderNativeIDs; RCTSwiftUIContainerViewWrapper *_swiftUIWrapper; + RCTBackgroundImageURLLoader *_backgroundImageLoader; } #ifdef RCT_DYNAMIC_FRAMEWORKS @@ -67,6 +68,8 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { _props = ViewShadowNode::defaultSharedProps(); _reactSubviews = [NSMutableArray new]; + _backgroundImageLoader = [RCTBackgroundImageURLLoader new]; + _backgroundImageLoader.delegate = self; self.multipleTouchEnabled = YES; _useCustomContainerView = NO; _removeClippedSubviews = NO; @@ -553,6 +556,13 @@ - (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter _eventEmitter = std::static_pointer_cast(eventEmitter); } +- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState +{ + auto newViewState = std::static_pointer_cast(state); + auto oldViewState = oldState ? std::static_pointer_cast(oldState) : nullptr; + [_backgroundImageLoader updateStateWithNewState:newViewState oldState:oldViewState]; +} + - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics { @@ -643,6 +653,9 @@ - (void)prepareForRecycle _filterLayer = nil; [self clearExistingBackgroundImageLayers]; + // Clean up background image observers + [_backgroundImageLoader reset]; + _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil; _eventEmitter.reset(); _isJSResponder = NO; @@ -1166,29 +1179,49 @@ - (void)invalidateLayer backgroundRepeat = _props->backgroundRepeat[imageIndex % _props->backgroundRepeat.size()]; } - CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea - itemIntrinsicSize:backgroundPositioningArea.size - backgroundSize:backgroundSize - backgroundRepeat:backgroundRepeat]; - - CALayer *gradientLayer; + CALayer *itemLayer = nil; if (std::holds_alternative(backgroundImage)) { + CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea + itemIntrinsicSize:backgroundPositioningArea.size + backgroundSize:backgroundSize + backgroundRepeat:backgroundRepeat]; const auto &linearGradient = std::get(backgroundImage); - gradientLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient]; + itemLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient]; } else if (std::holds_alternative(backgroundImage)) { + CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea + itemIntrinsicSize:backgroundPositioningArea.size + backgroundSize:backgroundSize + backgroundRepeat:backgroundRepeat]; const auto &radialGradient = std::get(backgroundImage); - gradientLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient]; + itemLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient]; + } else if (std::holds_alternative(backgroundImage)) { + const auto &urlBgImage = std::get(backgroundImage); + NSString *uri = [NSString stringWithUTF8String:urlBgImage.uri.c_str()]; + UIImage *loadedImage = [_backgroundImageLoader loadedImageForUri:uri]; + if (loadedImage != nil) { + CGSize intrinsicSize = loadedImage.size; + CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea + itemIntrinsicSize:intrinsicSize + backgroundSize:backgroundSize + backgroundRepeat:backgroundRepeat]; + CALayer *imageLayer = [CALayer layer]; + imageLayer.frame = CGRectMake(0, 0, backgroundImageSize.width, backgroundImageSize.height); + imageLayer.contents = (__bridge id)loadedImage.CGImage; + imageLayer.contentsGravity = kCAGravityResizeAspectFill; + itemLayer = imageLayer; + } } - if (gradientLayer != nil) { + if (itemLayer != nil) { + CGSize itemSize = itemLayer.frame.size; CALayer *backgroundImageLayer = [RCTBackgroundImageUtils createBackgroundImageLayerWithSize:backgroundPositioningArea paintingArea:backgroundPaintingArea - itemSize:backgroundImageSize + itemSize:itemSize backgroundPosition:backgroundPosition backgroundRepeat:backgroundRepeat - itemLayer:gradientLayer]; + itemLayer:itemLayer]; [self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetricsBI]; backgroundImageLayer.masksToBounds = YES; backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION; @@ -1302,6 +1335,13 @@ - (void)clearExistingBackgroundImageLayers [_backgroundImageLayers removeAllObjects]; } +#pragma mark - RCTBackgroundImageURLLoaderDelegate + +- (void)backgroundImagesDidLoad +{ + [self invalidateLayer]; +} + #pragma mark - Accessibility - (NSObject *)accessibilityElement diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 17d987fa6bd665..50338ab1bc6582 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -131,6 +131,9 @@ Pod::Spec.new do |s| sss.dependency "Yoga" sss.source_files = podspec_sources(["react/renderer/components/view/*.{m,mm,cpp,h}", "react/renderer/components/view/platform/cxx/**/*.{m,mm,cpp,h}"], ["react/renderer/components/view/*.{h}", "react/renderer/components/view/platform/cxx/**/*.{h}"]) sss.header_dir = "react/renderer/components/view" + sss.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/react/renderer/imagemanager/platform/ios\"" + } end ss.subspec "scrollview" do |sss| diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp new file mode 100644 index 00000000000000..5becd215fe123e --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp @@ -0,0 +1,28 @@ +/* + * 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 "ViewComponentDescriptor.h" +#include + +namespace facebook::react { + +extern const char ImageManagerKey[]; + +ViewComponentDescriptor::ViewComponentDescriptor( + const ComponentDescriptorParameters& parameters) + : ConcreteComponentDescriptor(parameters), + imageManager_( + getManagerByName(contextContainer_, ImageManagerKey)) {} + +void ViewComponentDescriptor::adopt(ShadowNode& shadowNode) const { + ConcreteComponentDescriptor::adopt(shadowNode); + + auto& viewShadowNode = static_cast(shadowNode); + viewShadowNode.setImageManager(imageManager_); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h index e1958134b4c8a4..f0e04ae020b414 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h @@ -12,12 +12,17 @@ namespace facebook::react { -class ViewComponentDescriptor : public ConcreteComponentDescriptor { +class ImageManager; + +class ViewComponentDescriptor + : public ConcreteComponentDescriptor { public: - ViewComponentDescriptor(const ComponentDescriptorParameters ¶meters) - : ConcreteComponentDescriptor(parameters) - { - } + ViewComponentDescriptor(const ComponentDescriptorParameters ¶meters); + + void adopt(ShadowNode &shadowNode) const override; + + private: + const std::shared_ptr imageManager_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index 641f544bee6209..4635b313459d02 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -8,6 +8,9 @@ #include "ViewShadowNode.h" #include #include +#include +#include +#include namespace facebook::react { @@ -92,4 +95,68 @@ void ViewShadowNode::initialize() noexcept { } } +void ViewShadowNode::setImageManager( + const std::shared_ptr& imageManager) { + ensureUnsealed(); + imageManager_ = imageManager; + updateStateIfNeeded(); +} + +void ViewShadowNode::updateStateIfNeeded() { + if (!imageManager_) { + return; + } + + ensureUnsealed(); + + const auto& viewProps = static_cast(*props_); + const auto& backgroundImages = viewProps.backgroundImage; + + std::vector newRequests; + for (const auto& bgImage : backgroundImages) { + if (std::holds_alternative(bgImage)) { + const auto& urlBgImage = std::get(bgImage); + if (!urlBgImage.uri.empty()) { + BackgroundImageURLRequest request; + request.imageSource.uri = urlBgImage.uri; + if (urlBgImage.uri.find("__packager_asset") != std::string::npos) { + request.imageSource.type = ImageSource::Type::Local; + } else { + request.imageSource.type = ImageSource::Type::Remote; + } + newRequests.push_back(std::move(request)); + } + } + } + + if (newRequests.empty()) { + return; + } + + const auto& savedState = getStateData(); + const auto& oldRequests = savedState.getBackgroundImageRequests(); + + bool requestsChanged = newRequests.size() != oldRequests.size(); + if (!requestsChanged) { + for (size_t i = 0; i < newRequests.size(); ++i) { + if (newRequests[i].imageSource != oldRequests[i].imageSource) { + requestsChanged = true; + break; + } + } + } + + if (!requestsChanged) { + return; + } + + for (auto& request : newRequests) { + request.imageRequest = std::make_shared( + imageManager_->requestImage(request.imageSource, getSurfaceId())); + } + + ViewState state{std::move(newRequests)}; + setStateData(std::move(state)); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h index b3195934440a19..dea202b5671298 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h @@ -9,9 +9,12 @@ #include #include +#include namespace facebook::react { +class ImageManager; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) extern const char ViewComponentName[]; @@ -30,14 +33,19 @@ class ViewShadowNodeProps final : public ViewProps { /* * `ShadowNode` for component. */ -class ViewShadowNode final : public ConcreteViewShadowNode { +class ViewShadowNode final : public ConcreteViewShadowNode { public: ViewShadowNode(const ShadowNodeFragment &fragment, const ShadowNodeFamily::Shared &family, ShadowNodeTraits traits); ViewShadowNode(const ShadowNode &sourceShadowNode, const ShadowNodeFragment &fragment); + void setImageManager(const std::shared_ptr &imageManager); + private: void initialize() noexcept; + void updateStateIfNeeded(); + + std::shared_ptr imageManager_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp new file mode 100644 index 00000000000000..01a4e024901f6a --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp @@ -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. + */ + +#include "ViewState.h" + +namespace facebook::react { + +const std::vector& ViewState::getBackgroundImageRequests() + const { + return backgroundImageRequests_; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h b/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h new file mode 100644 index 00000000000000..cef0682714fe08 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h @@ -0,0 +1,50 @@ +/* + * 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 + +#ifdef RN_SERIALIZABLE_STATE +#include +#endif + +namespace facebook::react { + +struct BackgroundImageURLRequest { + ImageSource imageSource{}; + std::shared_ptr imageRequest{}; + + bool operator==(const BackgroundImageURLRequest& rhs) const { + return imageSource == rhs.imageSource && imageRequest == rhs.imageRequest; + } +}; + +class ViewState final { + public: + ViewState() = default; + + explicit ViewState(std::vector backgroundImageRequests) + : backgroundImageRequests_(std::move(backgroundImageRequests)) {} + + const std::vector& getBackgroundImageRequests() const; + +#ifdef RN_SERIALIZABLE_STATE + ViewState(const ViewState& previousState, folly::dynamic data) {} + + folly::dynamic getDynamic() const { + return {}; + } +#endif + + private: + std::vector backgroundImageRequests_; +}; + +} // namespace facebook::react