From 55eb9debccafaa091c6fcb93db198295aedfb099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Mon, 29 Dec 2025 19:17:59 +0530 Subject: [PATCH 1/9] url() support in background image on ios --- packages/react-native/Package.swift | 1 + .../View/RCTBackgroundImageURLLoader.h | 30 ++++ .../View/RCTBackgroundImageURLLoader.mm | 159 ++++++++++++++++++ .../View/RCTViewComponentView.h | 4 +- .../View/RCTViewComponentView.mm | 68 ++++++-- .../ReactCommon/React-Fabric.podspec | 3 + .../renderer/components/view/CMakeLists.txt | 5 +- .../view/ViewComponentDescriptor.cpp | 34 ++++ .../components/view/ViewComponentDescriptor.h | 14 +- .../components/view/ViewShadowNode.cpp | 78 +++++++++ .../renderer/components/view/ViewShadowNode.h | 10 +- .../renderer/components/view/ViewState.cpp | 17 ++ .../renderer/components/view/ViewState.h | 50 ++++++ 13 files changed, 455 insertions(+), 18 deletions(-) create mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h create mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index ac1611c59c07..3582af3d0163 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -467,6 +467,7 @@ let reactCore = RNTarget( let reactFabric = RNTarget( name: .reactFabric, path: "ReactCommon/react/renderer", + searchPaths: ["ReactCommon/react/renderer/imagemanager/platform/ios"], excludedPaths: [ "animated/tests", "animationbackend/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 000000000000..ec99ac1b5cd3 --- /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 000000000000..6b91500167e5 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm @@ -0,0 +1,159 @@ +/* + * 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 +#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) { + // `addObserver` replays a cached image synchronously, so register every uri + // first. Otherwise the completion check in notifyDelegateIfAllImagesLoaded + // runs against a half-filled map. + for (const auto &request : *newRequests) { + if (request.imageRequest) { + _uriToObserver.try_emplace(request.imageSource.uri); + } + } + + for (const auto &request : *newRequests) { + if (request.imageRequest) { + auto it = _uriToObserver.find(request.imageSource.uri); + // A null entry is a uri that has not been attached yet. Duplicated uris + // share a single observer, matching the previous behaviour. + if (it != _uriToObserver.end() && it->second == nullptr) { + it->second = std::make_shared(self); + 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.get() == 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.get() == 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 124a85c65c17..da0837958acd 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -14,12 +14,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 8a074cd2b604..281eaf969b20 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -123,6 +123,7 @@ @implementation RCTViewComponentView { NSMutableSet *_accessibilityOrderNativeIDs; RCTSwiftUIContainerViewWrapper *_swiftUIWrapper; BOOL _focusable; + RCTBackgroundImageURLLoader *_backgroundImageLoader; } #ifdef RCT_DYNAMIC_FRAMEWORKS @@ -137,6 +138,8 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { _props = ViewShadowNode::defaultSharedProps(); _reactSubviews = [NSMutableArray new]; + _backgroundImageLoader = [RCTBackgroundImageURLLoader new]; + _backgroundImageLoader.delegate = self; #if !TARGET_OS_TV self.multipleTouchEnabled = YES; #endif @@ -691,6 +694,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 { @@ -884,6 +894,9 @@ - (void)prepareForRecycle [self clearExistingBackgroundImageLayers]; [self _setLastSentSafeAreaInsets:nil]; + // Clean up background image observers + [_backgroundImageLoader reset]; + _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil; _eventEmitter.reset(); _isJSResponder = NO; @@ -1419,29 +1432,55 @@ - (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) { + // Bundled assets carry their natural size in layout units. A plain url + // has none, so fall back to the decoded image. + CGSize intrinsicSize = (urlBgImage.intrinsicWidth > 0 && urlBgImage.intrinsicHeight > 0) + ? CGSizeMake(urlBgImage.intrinsicWidth, urlBgImage.intrinsicHeight) + : 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; @@ -1555,6 +1594,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 ce02f4138872..97a841124f1e 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -167,6 +167,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 "viewUmbrella" do |sss| diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt index 6e05d8d09599..a3db98a5d808 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt @@ -22,7 +22,10 @@ add_library(rrc_view OBJECT ${rrc_view_SRC}) react_native_android_selector(platform_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platform/android/ ${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/) -target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR}) +react_native_android_selector(imagemanager_platform_DIR + ${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/android/ + ${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/cxx/) +target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR} ${imagemanager_platform_DIR}) target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR}/react/renderer/components/view) target_link_libraries(rrc_view 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 000000000000..5c7a2f2055cc --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp @@ -0,0 +1,34 @@ +/* + * 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)) {} + +State::Shared ViewComponentDescriptor::createInitialState( + const Props::Shared& /*props*/, + const ShadowNodeFamily::Shared& /*family*/) const { + return nullptr; +} + +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 d6cba8f4d448..420e50a10b3f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h @@ -14,12 +14,18 @@ namespace facebook::react { +class ImageManager; + class ViewComponentDescriptor : public ConcreteComponentDescriptor { public: - ViewComponentDescriptor(const ComponentDescriptorParameters ¶meters) - : ConcreteComponentDescriptor(parameters) - { - } + ViewComponentDescriptor(const ComponentDescriptorParameters ¶meters); + + void adopt(ShadowNode &shadowNode) const override; + + State::Shared createInitialState(const Props::Shared &props, const ShadowNodeFamily::Shared &family) 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 b3d396e263c1..228c5a479e16 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 { @@ -88,4 +91,79 @@ 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; + // `resolveAssetSource` already turns bundled assets into a uri on the + // JS side, so everything reaching here is addressed by url. + request.imageSource.type = ImageSource::Type::Remote; + // ImageSource::scale defaults to 3, which would make the decoded image + // report a third of its real size. Bundled assets carry their natural + // size in the props instead, so treat the decoded pixels as 1x here. + request.imageSource.scale = 1; + newRequests.push_back(std::move(request)); + } + } + } + + if (newRequests.empty()) { + return; + } + + // A view only gets state once it has a url() background image, so there may + // be nothing to compare against yet. + if (state_) { + 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)}; + if (state_) { + setStateData(std::move(state)); + } else { + state_ = std::make_shared( + std::make_shared(std::move(state)), getFamilyShared()); + } +} + } // 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 689dfbbf29bf..80c1776536a3 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h @@ -11,9 +11,12 @@ #include #include +#include namespace facebook::react { +class ImageManager; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) extern const char ViewComponentName[]; @@ -22,14 +25,19 @@ using ViewShadowNodeProps = 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 000000000000..01a4e024901f --- /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 000000000000..cef0682714fe --- /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 From 83c2b1defd8871b3b14fea6e0021b5fd26015ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 29 Oct 2026 07:49:08 +0530 Subject: [PATCH 2/9] Load background url() images at mount on iOS Move url() loading out of ViewShadowNode, ViewState, and ViewComponentDescriptor. RCTViewComponentView now starts the requests in finalizeUpdates through RCTBackgroundImageURLLoader, using the ImageManager that RCTMountingManager passes to the component view registry. No shared C++ or build file changes remain. --- packages/react-native/Package.swift | 1 - .../View/RCTBackgroundImageURLLoader.h | 30 --- .../View/RCTBackgroundImageURLLoader.mm | 159 -------------- .../View/RCTViewComponentView.h | 14 +- .../View/RCTViewComponentView.mm | 114 +++++----- .../Mounting/RCTComponentViewRegistry.h | 12 ++ .../Mounting/RCTComponentViewRegistry.mm | 12 +- .../Fabric/Mounting/RCTMountingManager.mm | 7 + .../Utils/RCTBackgroundImageURLLoader.h | 48 +++++ .../Utils/RCTBackgroundImageURLLoader.mm | 196 ++++++++++++++++++ .../ReactCommon/React-Fabric.podspec | 3 - .../renderer/components/view/CMakeLists.txt | 5 +- .../view/ViewComponentDescriptor.cpp | 34 --- .../components/view/ViewComponentDescriptor.h | 14 +- .../components/view/ViewShadowNode.cpp | 78 ------- .../renderer/components/view/ViewShadowNode.h | 10 +- .../renderer/components/view/ViewState.cpp | 17 -- .../renderer/components/view/ViewState.h | 50 ----- .../api-snapshots/ReactAppleDebugCxx.api | 8 + .../api-snapshots/ReactAppleFrameworksCxx.api | 8 + .../api-snapshots/ReactAppleNewarchCxx.api | 8 + .../api-snapshots/ReactAppleReleaseCxx.api | 8 + 22 files changed, 385 insertions(+), 451 deletions(-) delete mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h delete mode 100644 packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm create mode 100644 packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h create mode 100644 packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm delete mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp delete mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp delete mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index 3582af3d0163..ac1611c59c07 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -467,7 +467,6 @@ let reactCore = RNTarget( let reactFabric = RNTarget( name: .reactFabric, path: "ReactCommon/react/renderer", - searchPaths: ["ReactCommon/react/renderer/imagemanager/platform/ios"], excludedPaths: [ "animated/tests", "animationbackend/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 deleted file mode 100644 index ec99ac1b5cd3..000000000000 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6b91500167e5..000000000000 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTBackgroundImageURLLoader.mm +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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 -#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) { - // `addObserver` replays a cached image synchronously, so register every uri - // first. Otherwise the completion check in notifyDelegateIfAllImagesLoaded - // runs against a half-filled map. - for (const auto &request : *newRequests) { - if (request.imageRequest) { - _uriToObserver.try_emplace(request.imageSource.uri); - } - } - - for (const auto &request : *newRequests) { - if (request.imageRequest) { - auto it = _uriToObserver.find(request.imageSource.uri); - // A null entry is a uri that has not been attached yet. Duplicated uris - // share a single observer, matching the previous behaviour. - if (it != _uriToObserver.end() && it->second == nullptr) { - it->second = std::make_shared(self); - 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.get() == 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.get() == 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 da0837958acd..379439847bae 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -14,14 +14,18 @@ #import #import -#import "RCTBackgroundImageURLLoader.h" +#include + +namespace facebook::react { +class ImageManager; +} // namespace facebook::react NS_ASSUME_NONNULL_BEGIN /** * UIView class for component. */ -@interface RCTViewComponentView : UIView { +@interface RCTViewComponentView : UIView { @protected facebook::react::LayoutMetrics _layoutMetrics; facebook::react::SharedViewProps _props; @@ -62,6 +66,12 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; +/** + * Used to load the `url()` entries of `backgroundImage`. + * Set by `RCTComponentViewRegistry` when it creates the view. + */ +- (void)setImageManager:(std::shared_ptr)imageManager; + /** * Enforcing `call super` semantic for overridden methods from `RCTComponentViewProtocol`. * The methods update the instance variables. 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 281eaf969b20..53ea18c1f508 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -11,12 +11,14 @@ #import #import #import +#import #import #import #import #import #import +#import #import #import #import @@ -123,7 +125,8 @@ @implementation RCTViewComponentView { NSMutableSet *_accessibilityOrderNativeIDs; RCTSwiftUIContainerViewWrapper *_swiftUIWrapper; BOOL _focusable; - RCTBackgroundImageURLLoader *_backgroundImageLoader; + std::shared_ptr _imageManager; + RCTBackgroundImageURLLoader *_backgroundImageURLLoader; } #ifdef RCT_DYNAMIC_FRAMEWORKS @@ -138,8 +141,6 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { _props = ViewShadowNode::defaultSharedProps(); _reactSubviews = [NSMutableArray new]; - _backgroundImageLoader = [RCTBackgroundImageURLLoader new]; - _backgroundImageLoader.delegate = self; #if !TARGET_OS_TV self.multipleTouchEnabled = YES; #endif @@ -694,13 +695,6 @@ - (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 { @@ -852,6 +846,9 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask { [super finalizeUpdates:updateMask]; _useCustomContainerView = [self styleWouldClipOverflowInk]; + if ((updateMask & RNComponentViewUpdateMaskProps) != 0) { + [self updateBackgroundImageURLRequests]; + } if (!_needsInvalidateLayer) { return; } @@ -892,11 +889,9 @@ - (void)prepareForRecycle [_filterLayer removeFromSuperlayer]; _filterLayer = nil; [self clearExistingBackgroundImageLayers]; + _backgroundImageURLLoader = nil; [self _setLastSentSafeAreaInsets:nil]; - // Clean up background image observers - [_backgroundImageLoader reset]; - _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil; _eventEmitter.reset(); _isJSResponder = NO; @@ -1432,52 +1427,43 @@ - (void)invalidateLayer backgroundRepeat = _props->backgroundRepeat[imageIndex % _props->backgroundRepeat.size()]; } - CALayer *itemLayer = nil; + CGSize itemIntrinsicSize = backgroundPositioningArea.size; + UIImage *urlImage = nil; + if (const auto *urlBackgroundImage = std::get_if(&backgroundImage)) { + urlImage = [_backgroundImageURLLoader imageForURI:urlBackgroundImage->uri]; + // Bundled assets send their natural size in layout units. A plain url + // has none, so use the size of the decoded image. + if (urlBackgroundImage->intrinsicWidth > 0 && urlBackgroundImage->intrinsicHeight > 0) { + itemIntrinsicSize = CGSizeMake(urlBackgroundImage->intrinsicWidth, urlBackgroundImage->intrinsicHeight); + } else if (urlImage != nil) { + itemIntrinsicSize = urlImage.size; + } + } + + CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea + itemIntrinsicSize:itemIntrinsicSize + backgroundSize:backgroundSize + backgroundRepeat:backgroundRepeat]; + + CALayer *itemLayer; if (std::holds_alternative(backgroundImage)) { - CGSize backgroundImageSize = - [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea - itemIntrinsicSize:backgroundPositioningArea.size - backgroundSize:backgroundSize - backgroundRepeat:backgroundRepeat]; const auto &linearGradient = std::get(backgroundImage); 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); 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) { - // Bundled assets carry their natural size in layout units. A plain url - // has none, so fall back to the decoded image. - CGSize intrinsicSize = (urlBgImage.intrinsicWidth > 0 && urlBgImage.intrinsicHeight > 0) - ? CGSizeMake(urlBgImage.intrinsicWidth, urlBgImage.intrinsicHeight) - : 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; - } + } else if (urlImage != nil) { + itemLayer = [CALayer layer]; + itemLayer.frame = CGRectMake(0, 0, backgroundImageSize.width, backgroundImageSize.height); + itemLayer.contents = (__bridge id)urlImage.CGImage; } if (itemLayer != nil) { - CGSize itemSize = itemLayer.frame.size; CALayer *backgroundImageLayer = [RCTBackgroundImageUtils createBackgroundImageLayerWithSize:backgroundPositioningArea paintingArea:backgroundPaintingArea - itemSize:itemSize + itemSize:backgroundImageSize backgroundPosition:backgroundPosition backgroundRepeat:backgroundRepeat itemLayer:itemLayer]; @@ -1582,6 +1568,35 @@ - (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)c return maskLayer; } +- (void)setImageManager:(std::shared_ptr)imageManager +{ + _imageManager = std::move(imageManager); +} + +- (void)updateBackgroundImageURLRequests +{ + bool hasURLBackgroundImage = std::ranges::any_of(_props->backgroundImage, [](const auto &backgroundImage) { + return std::holds_alternative(backgroundImage); + }); + if (!hasURLBackgroundImage || !_imageManager) { + // Releasing it cancels all loads + _backgroundImageURLLoader = nil; + return; + } + + if (_backgroundImageURLLoader == nil) { + __weak RCTViewComponentView *weakSelf = self; + _backgroundImageURLLoader = [[RCTBackgroundImageURLLoader alloc] initWithImageManager:_imageManager + onLoad:^{ + [weakSelf invalidateLayer]; + }]; + } + + const auto &eventTarget = _eventEmitter ? _eventEmitter->getEventTarget() : nullptr; + [_backgroundImageURLLoader loadBackgroundImages:_props->backgroundImage + surfaceId:eventTarget ? eventTarget->getSurfaceId() : -1]; +} + - (void)clearExistingBackgroundImageLayers { if (_backgroundImageLayers == nil) { @@ -1594,13 +1609,6 @@ - (void)clearExistingBackgroundImageLayers [_backgroundImageLayers removeAllObjects]; } -#pragma mark - RCTBackgroundImageURLLoaderDelegate - -- (void)backgroundImagesDidLoad -{ - [self invalidateLayer]; -} - #pragma mark - Accessibility - (NSObject *)accessibilityElement diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h index 584c4cb7ea97..dea33bb434ab 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.h @@ -12,6 +12,12 @@ #import #import +#include + +namespace facebook::react { +class ImageManager; +} // namespace facebook::react + NS_ASSUME_NONNULL_BEGIN /** @@ -50,6 +56,12 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable UIView *)findComponentViewWithTag:(facebook::react::Tag)tag; +/** + * Passed to every `RCTViewComponentView` the registry creates, to load + * `url()` background images. + */ +- (void)setImageManager:(std::shared_ptr)imageManager; + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm index 0834bbfb7d14..8eb27e18cb34 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm @@ -24,6 +24,7 @@ @implementation RCTComponentViewRegistry { std::unordered_map _registry; std::unordered_map> _recyclePool; + std::shared_ptr _imageManager; } - (instancetype)init @@ -93,7 +94,11 @@ - (RCTComponentViewDescriptor)_dequeueComponentViewWithComponentHandle:(Componen auto &recycledViews = _recyclePool[componentHandle]; if (recycledViews.empty()) { - return [self.componentViewFactory createComponentViewWithComponentHandle:componentHandle]; + auto componentViewDescriptor = [self.componentViewFactory createComponentViewWithComponentHandle:componentHandle]; + if (_imageManager && [componentViewDescriptor.view isKindOfClass:[RCTViewComponentView class]]) { + [(RCTViewComponentView *)componentViewDescriptor.view setImageManager:_imageManager]; + } + return componentViewDescriptor; } auto componentViewDescriptor = recycledViews.back(); @@ -119,6 +124,11 @@ - (void)_enqueueComponentViewWithComponentHandle:(ComponentHandle)componentHandl recycledViews.push_back(componentViewDescriptor); } +- (void)setImageManager:(std::shared_ptr)imageManager +{ + _imageManager = std::move(imageManager); +} + - (void)handleApplicationDidReceiveMemoryWarningNotification { _recyclePool.clear(); diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm index 68662f06d59b..0761be81ceca 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm @@ -15,7 +15,9 @@ #import #import #import +#import #import +#import #import #import #import @@ -159,6 +161,11 @@ - (instancetype)init - (void)setContextContainer:(std::shared_ptr)contextContainer { _contextContainer = contextContainer; + + // `ImageManager` requires the image loader, which some hosts do not provide. + if (_contextContainer && _contextContainer->find>("RCTImageLoader").has_value()) { + [_componentViewRegistry setImageManager:getManagerByName(_contextContainer, ImageManagerKey)]; + } } - (void)attachSurfaceToView:(UIView *)view surfaceId:(SurfaceId)surfaceId diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h new file mode 100644 index 000000000000..5a5808c8a1a1 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h @@ -0,0 +1,48 @@ +/* + * 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 + +#include +#include +#include + +namespace facebook::react { +class ImageManager; +} // namespace facebook::react + +NS_ASSUME_NONNULL_BEGIN + +/* + * Loads the `url()` entries of a view's `backgroundImage`. + * Releasing the loader cancels its requests. + */ +@interface RCTBackgroundImageURLLoader : NSObject + +/* + * `onLoad` is called on the main queue each time an image finishes loading. + */ +- (instancetype)initWithImageManager:(std::shared_ptr)imageManager + onLoad:(void (^)(void))onLoad; + +/* + * Requests every `url()` entry in `backgroundImages`. Does nothing if the + * urls did not change since the last call. + */ +- (void)loadBackgroundImages:(const std::vector &)backgroundImages + surfaceId:(facebook::react::SurfaceId)surfaceId; + +/* + * Returns nil until the image for `uri` has loaded. + */ +- (nullable UIImage *)imageForURI:(const std::string &)uri; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm new file mode 100644 index 000000000000..cae13bc29fd4 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm @@ -0,0 +1,196 @@ +/* + * 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 +#import +#import + +#include +#include + +using namespace facebook::react; + +/* + * The image for one uri. It is the delegate of its own observer proxy. A + * callback that is queued on the main queue retains it, so the callback + * always reaches the entry it was made for, even after a cancel. + */ +@interface RCTBackgroundImageURLEntry : NSObject + +@property (nonatomic, readonly) std::string uri; +@property (nonatomic, readonly, nullable) UIImage *image; + +- (instancetype)initWithURI:(std::string)uri onLoad:(void (^)(void))onLoad; +- (void)startWithImageManager:(const ImageManager &)imageManager surfaceId:(SurfaceId)surfaceId; +- (void)cancel; + +@end + +@implementation RCTBackgroundImageURLEntry { + void (^_onLoad)(void); + std::optional _imageRequest; + std::shared_ptr _observer; + BOOL _cancelled; +} + +- (instancetype)initWithURI:(std::string)uri onLoad:(void (^)(void))onLoad +{ + if (self = [super init]) { + _uri = std::move(uri); + _onLoad = onLoad; + } + return self; +} + +- (void)dealloc +{ + [self cancel]; +} + +- (void)startWithImageManager:(const ImageManager &)imageManager surfaceId:(SurfaceId)surfaceId +{ + ImageSource imageSource; + imageSource.type = ImageSource::Type::Remote; + imageSource.uri = _uri; + // CSS treats one image pixel as one point. Bundled assets send their + // natural size in props instead. + imageSource.scale = 1; + _imageRequest.emplace(imageManager.requestImage(imageSource, surfaceId)); + _observer = std::make_shared(self); + // Can deliver a cached response synchronously. + _imageRequest->getObserverCoordinator().addObserver(_observer); +} + +- (void)cancel +{ + _cancelled = YES; + if (_imageRequest) { + // Removing the last observer cancels the load if it is still running. + _imageRequest->getObserverCoordinator().removeObserver(_observer); + } +} + +#pragma mark - RCTImageResponseDelegate + +- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer +{ + if (_cancelled) { + return; + } + _image = image; + _onLoad(); +} + +- (void)didReceiveProgress:(float)progress + loaded:(int64_t)loaded + total:(int64_t)total + fromObserver:(const void *)observer +{ +} + +- (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer +{ + if (_cancelled) { + return; + } + RCTLogWarn(@"Failed to load background image: %s - %@", _uri.c_str(), error); +} + +@end + +@implementation RCTBackgroundImageURLLoader { + std::shared_ptr _imageManager; + void (^_onLoad)(void); + NSArray *_entries; +} + +- (instancetype)initWithImageManager:(std::shared_ptr)imageManager onLoad:(void (^)(void))onLoad +{ + if (self = [super init]) { + _imageManager = std::move(imageManager); + _onLoad = onLoad; + _entries = @[]; + } + return self; +} + +- (void)dealloc +{ + for (RCTBackgroundImageURLEntry *entry in _entries) { + [entry cancel]; + } +} + +- (void)loadBackgroundImages:(const std::vector &)backgroundImages surfaceId:(SurfaceId)surfaceId +{ + RCTAssertMainQueue(); + + std::vector uris; + for (const auto &backgroundImage : backgroundImages) { + if (const auto *urlBackgroundImage = std::get_if(&backgroundImage)) { + const auto &uri = urlBackgroundImage->uri; + if (!uri.empty() && std::ranges::find(uris, uri) == uris.end()) { + uris.push_back(uri); + } + } + } + + BOOL unchanged = uris.size() == _entries.count; + for (NSUInteger i = 0; unchanged && i < uris.size(); i++) { + unchanged = _entries[i].uri == uris[i]; + } + if (unchanged) { + return; + } + + // Keep the entries for uris that are still used, so their images do not + // disappear while the new ones load. + NSMutableArray *entries = [NSMutableArray arrayWithCapacity:uris.size()]; + NSMutableArray *newEntries = [NSMutableArray new]; + for (const auto &uri : uris) { + RCTBackgroundImageURLEntry *entry = [self entryForURI:uri]; + if (entry == nil) { + entry = [[RCTBackgroundImageURLEntry alloc] initWithURI:uri onLoad:_onLoad]; + [newEntries addObject:entry]; + } + [entries addObject:entry]; + } + + for (RCTBackgroundImageURLEntry *entry in _entries) { + if (![entries containsObject:entry]) { + [entry cancel]; + } + } + + // Start only after `_entries` is updated, because a cached response can + // arrive synchronously and call `onLoad`. + _entries = entries; + for (RCTBackgroundImageURLEntry *entry in newEntries) { + [entry startWithImageManager:*_imageManager surfaceId:surfaceId]; + } +} + +- (UIImage *)imageForURI:(const std::string &)uri +{ + return [self entryForURI:uri].image; +} + +- (nullable RCTBackgroundImageURLEntry *)entryForURI:(const std::string &)uri +{ + for (RCTBackgroundImageURLEntry *entry in _entries) { + if (entry.uri == uri) { + return entry; + } + } + return nil; +} + +@end diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 97a841124f1e..ce02f4138872 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -167,9 +167,6 @@ 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 "viewUmbrella" do |sss| diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt index a3db98a5d808..6e05d8d09599 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt @@ -22,10 +22,7 @@ add_library(rrc_view OBJECT ${rrc_view_SRC}) react_native_android_selector(platform_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platform/android/ ${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/) -react_native_android_selector(imagemanager_platform_DIR - ${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/android/ - ${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/cxx/) -target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR} ${imagemanager_platform_DIR}) +target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR}) target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR}/react/renderer/components/view) target_link_libraries(rrc_view diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp deleted file mode 100644 index 5c7a2f2055cc..000000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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)) {} - -State::Shared ViewComponentDescriptor::createInitialState( - const Props::Shared& /*props*/, - const ShadowNodeFamily::Shared& /*family*/) const { - return nullptr; -} - -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 420e50a10b3f..d6cba8f4d448 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewComponentDescriptor.h @@ -14,18 +14,12 @@ namespace facebook::react { -class ImageManager; - class ViewComponentDescriptor : public ConcreteComponentDescriptor { public: - ViewComponentDescriptor(const ComponentDescriptorParameters ¶meters); - - void adopt(ShadowNode &shadowNode) const override; - - State::Shared createInitialState(const Props::Shared &props, const ShadowNodeFamily::Shared &family) const override; - - private: - const std::shared_ptr imageManager_; + ViewComponentDescriptor(const ComponentDescriptorParameters ¶meters) + : ConcreteComponentDescriptor(parameters) + { + } }; } // 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 228c5a479e16..b3d396e263c1 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -8,9 +8,6 @@ #include "ViewShadowNode.h" #include #include -#include -#include -#include namespace facebook::react { @@ -91,79 +88,4 @@ 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; - // `resolveAssetSource` already turns bundled assets into a uri on the - // JS side, so everything reaching here is addressed by url. - request.imageSource.type = ImageSource::Type::Remote; - // ImageSource::scale defaults to 3, which would make the decoded image - // report a third of its real size. Bundled assets carry their natural - // size in the props instead, so treat the decoded pixels as 1x here. - request.imageSource.scale = 1; - newRequests.push_back(std::move(request)); - } - } - } - - if (newRequests.empty()) { - return; - } - - // A view only gets state once it has a url() background image, so there may - // be nothing to compare against yet. - if (state_) { - 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)}; - if (state_) { - setStateData(std::move(state)); - } else { - state_ = std::make_shared( - std::make_shared(std::move(state)), getFamilyShared()); - } -} - } // 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 80c1776536a3..689dfbbf29bf 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.h @@ -11,12 +11,9 @@ #include #include -#include namespace facebook::react { -class ImageManager; - // NOLINTNEXTLINE(modernize-avoid-c-arrays) extern const char ViewComponentName[]; @@ -25,19 +22,14 @@ using ViewShadowNodeProps = 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 deleted file mode 100644 index 01a4e024901f..000000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.cpp +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 deleted file mode 100644 index cef0682714fe..000000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewState.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index e04f3b738889..c3f665a384b2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -653,6 +653,12 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public virtual _Nullable UIImage* imageForURI:(const std::string& uri); + public virtual instancetype initWithImageManager:onLoad:(std::shared_ptr imageManager, void(^)(void) onLoad); + public virtual void loadBackgroundImages:surfaceId:(const std::vector& backgroundImages, facebook::react::SurfaceId surfaceId); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); @@ -828,6 +834,7 @@ interface RCTComponentViewRegistry : public NSObject { public virtual const RCTComponentViewDescriptor& componentViewDescriptorWithTag:(facebook::react::Tag tag); public virtual const RCTComponentViewDescriptor& dequeueComponentViewWithComponentHandle:tag:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag); public virtual void enqueueComponentViewWithComponentHandle:tag:componentViewDescriptor:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag, RCTComponentViewDescriptor componentViewDescriptor); + public virtual void setImageManager:(std::shared_ptr imageManager); } interface RCTComposedViewRegistry : public NSMutableDictionary { @@ -2066,6 +2073,7 @@ interface RCTViewComponentView : public UIView imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index cb8ca685d4f2..a41d348a4717 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -662,6 +662,12 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public virtual _Nullable UIImage* imageForURI:(const std::string& uri); + public virtual instancetype initWithImageManager:onLoad:(std::shared_ptr imageManager, void(^)(void) onLoad); + public virtual void loadBackgroundImages:surfaceId:(const std::vector& backgroundImages, facebook::react::SurfaceId surfaceId); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); @@ -837,6 +843,7 @@ interface RCTComponentViewRegistry : public NSObject { public virtual const RCTComponentViewDescriptor& componentViewDescriptorWithTag:(facebook::react::Tag tag); public virtual const RCTComponentViewDescriptor& dequeueComponentViewWithComponentHandle:tag:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag); public virtual void enqueueComponentViewWithComponentHandle:tag:componentViewDescriptor:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag, RCTComponentViewDescriptor componentViewDescriptor); + public virtual void setImageManager:(std::shared_ptr imageManager); } interface RCTComposedViewRegistry : public NSMutableDictionary { @@ -2124,6 +2131,7 @@ interface RCTViewComponentView : public UIView imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 9010fcf23951..b3a25a02de8e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -653,6 +653,12 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public virtual _Nullable UIImage* imageForURI:(const std::string& uri); + public virtual instancetype initWithImageManager:onLoad:(std::shared_ptr imageManager, void(^)(void) onLoad); + public virtual void loadBackgroundImages:surfaceId:(const std::vector& backgroundImages, facebook::react::SurfaceId surfaceId); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); @@ -828,6 +834,7 @@ interface RCTComponentViewRegistry : public NSObject { public virtual const RCTComponentViewDescriptor& componentViewDescriptorWithTag:(facebook::react::Tag tag); public virtual const RCTComponentViewDescriptor& dequeueComponentViewWithComponentHandle:tag:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag); public virtual void enqueueComponentViewWithComponentHandle:tag:componentViewDescriptor:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag, RCTComponentViewDescriptor componentViewDescriptor); + public virtual void setImageManager:(std::shared_ptr imageManager); } interface RCTComposedViewRegistry : public NSMutableDictionary { @@ -2059,6 +2066,7 @@ interface RCTViewComponentView : public UIView imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 6bb9397bd2e4..1ed42e507907 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -653,6 +653,12 @@ interface RCTBackedTextViewDelegateAdapter : public NSObject { public virtual void skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange* textRange); } +interface RCTBackgroundImageURLLoader : public NSObject { + public virtual _Nullable UIImage* imageForURI:(const std::string& uri); + public virtual instancetype initWithImageManager:onLoad:(std::shared_ptr imageManager, void(^)(void) onLoad); + public virtual void loadBackgroundImages:surfaceId:(const std::vector& backgroundImages, facebook::react::SurfaceId surfaceId); +} + interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); @@ -828,6 +834,7 @@ interface RCTComponentViewRegistry : public NSObject { public virtual const RCTComponentViewDescriptor& componentViewDescriptorWithTag:(facebook::react::Tag tag); public virtual const RCTComponentViewDescriptor& dequeueComponentViewWithComponentHandle:tag:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag); public virtual void enqueueComponentViewWithComponentHandle:tag:componentViewDescriptor:(facebook::react::ComponentHandle componentHandle, facebook::react::Tag tag, RCTComponentViewDescriptor componentViewDescriptor); + public virtual void setImageManager:(std::shared_ptr imageManager); } interface RCTComposedViewRegistry : public NSMutableDictionary { @@ -2066,6 +2073,7 @@ interface RCTViewComponentView : public UIView imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); From bf489c0a51ffa7b8ba12b60230eb04e9c91ab455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 29 Oct 2026 08:38:32 +0530 Subject: [PATCH 3/9] Share one ImageManager and keep EXIF orientation for background url() RCTSurfacePresenter puts one ImageManager into the context container, so ImageComponentDescriptor and RCTMountingManager use the same instance and RCTImageLoader gets requests from one serial queue only. Background images that are not UIImageOrientationUp are drawn once into an upright image, because the layer draws the CGImage, which ignores imageOrientation. --- .../React/Fabric/Mounting/RCTMountingManager.mm | 8 ++++---- .../React/Fabric/RCTSurfacePresenter.mm | 8 ++++++++ .../Fabric/Utils/RCTBackgroundImageURLLoader.mm | 16 +++++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm index 0761be81ceca..bd0686bf73f1 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm @@ -17,7 +17,6 @@ #import #import #import -#import #import #import #import @@ -162,9 +161,10 @@ - (void)setContextContainer:(std::shared_ptr)contextCont { _contextContainer = contextContainer; - // `ImageManager` requires the image loader, which some hosts do not provide. - if (_contextContainer && _contextContainer->find>("RCTImageLoader").has_value()) { - [_componentViewRegistry setImageManager:getManagerByName(_contextContainer, ImageManagerKey)]; + if (_contextContainer) { + if (auto imageManager = _contextContainer->find>(ImageManagerKey)) { + [_componentViewRegistry setImageManager:*imageManager]; + } } } diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index e5b19bd47658..683f99f07065 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -29,7 +29,9 @@ #import #import +#import #import +#import #import #import #import @@ -96,6 +98,12 @@ - (instancetype)initWithContextContainer:(std::shared_ptr` and `url()` background images, because `RCTImageLoader` must not get + // requests from more than one queue. Its constructor requires the image loader. + if (contextContainer->find>("RCTImageLoader").has_value()) { + contextContainer->insert(ImageManagerKey, std::make_shared(contextContainer)); + } + _surfaceRegistry = [RCTSurfaceRegistry new]; _mountingManager = [RCTMountingManager new]; _mountingManager.contextContainer = contextContainer; diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm index cae13bc29fd4..60160388108d 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm @@ -18,6 +18,20 @@ using namespace facebook::react; +// Layers draw the `CGImage`, which ignores `imageOrientation`. +static UIImage *RCTUprightImage(UIImage *image) +{ + if (image.imageOrientation == UIImageOrientationUp) { + return image; + } + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat]; + format.scale = image.scale; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:image.size format:format]; + return [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + }]; +} + /* * The image for one uri. It is the delegate of its own observer proxy. A * callback that is queued on the main queue retains it, so the callback @@ -85,7 +99,7 @@ - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(con if (_cancelled) { return; } - _image = image; + _image = RCTUprightImage(image); _onLoad(); } From b9c01b0b5519ff26c8b0bf723192f36171d74423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 29 Oct 2026 09:20:45 +0530 Subject: [PATCH 4/9] Keep the aspect ratio for auto background sizes of url() images An auto dimension in backgroundSize now uses the image's aspect ratio, including after round repeat, as CSS does. Gradients have no ratio and keep their current behavior. Rename -[RCTViewComponentView setImageManager:] to setBackgroundImageManager:, so it cannot collide with a subclass property, and redraw rotated images off the main queue. --- .../View/RCTViewComponentView.h | 2 +- .../View/RCTViewComponentView.mm | 6 +++- .../Mounting/RCTComponentViewRegistry.mm | 2 +- .../Utils/RCTBackgroundImageURLLoader.mm | 30 ++++++++++++++----- .../Fabric/Utils/RCTBackgroundImageUtils.h | 1 + .../Fabric/Utils/RCTBackgroundImageUtils.mm | 23 ++++++++++++++ .../api-snapshots/ReactAppleDebugCxx.api | 4 +-- .../api-snapshots/ReactAppleFrameworksCxx.api | 4 +-- .../api-snapshots/ReactAppleNewarchCxx.api | 4 +-- .../api-snapshots/ReactAppleReleaseCxx.api | 4 +-- 10 files changed, 61 insertions(+), 19 deletions(-) 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 379439847bae..6b4076ffcee4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -70,7 +70,7 @@ NS_ASSUME_NONNULL_BEGIN * Used to load the `url()` entries of `backgroundImage`. * Set by `RCTComponentViewRegistry` when it creates the view. */ -- (void)setImageManager:(std::shared_ptr)imageManager; +- (void)setBackgroundImageManager:(std::shared_ptr)imageManager; /** * Enforcing `call super` semantic for overridden methods from `RCTComponentViewProtocol`. 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 53ea18c1f508..252412ab462a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1428,6 +1428,7 @@ - (void)invalidateLayer } CGSize itemIntrinsicSize = backgroundPositioningArea.size; + BOOL itemHasIntrinsicRatio = NO; UIImage *urlImage = nil; if (const auto *urlBackgroundImage = std::get_if(&backgroundImage)) { urlImage = [_backgroundImageURLLoader imageForURI:urlBackgroundImage->uri]; @@ -1435,13 +1436,16 @@ - (void)invalidateLayer // has none, so use the size of the decoded image. if (urlBackgroundImage->intrinsicWidth > 0 && urlBackgroundImage->intrinsicHeight > 0) { itemIntrinsicSize = CGSizeMake(urlBackgroundImage->intrinsicWidth, urlBackgroundImage->intrinsicHeight); + itemHasIntrinsicRatio = YES; } else if (urlImage != nil) { itemIntrinsicSize = urlImage.size; + itemHasIntrinsicRatio = YES; } } CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea itemIntrinsicSize:itemIntrinsicSize + itemHasIntrinsicRatio:itemHasIntrinsicRatio backgroundSize:backgroundSize backgroundRepeat:backgroundRepeat]; @@ -1568,7 +1572,7 @@ - (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)c return maskLayer; } -- (void)setImageManager:(std::shared_ptr)imageManager +- (void)setBackgroundImageManager:(std::shared_ptr)imageManager { _imageManager = std::move(imageManager); } diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm index 8eb27e18cb34..8bf55b6a20be 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewRegistry.mm @@ -96,7 +96,7 @@ - (RCTComponentViewDescriptor)_dequeueComponentViewWithComponentHandle:(Componen if (recycledViews.empty()) { auto componentViewDescriptor = [self.componentViewFactory createComponentViewWithComponentHandle:componentHandle]; if (_imageManager && [componentViewDescriptor.view isKindOfClass:[RCTViewComponentView class]]) { - [(RCTViewComponentView *)componentViewDescriptor.view setImageManager:_imageManager]; + [(RCTViewComponentView *)componentViewDescriptor.view setBackgroundImageManager:_imageManager]; } return componentViewDescriptor; } diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm index 60160388108d..4b63fee6bdd9 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm @@ -19,13 +19,8 @@ using namespace facebook::react; // Layers draw the `CGImage`, which ignores `imageOrientation`. -static UIImage *RCTUprightImage(UIImage *image) +static UIImage *RCTUprightImage(UIImage *image, UIGraphicsImageRendererFormat *format) { - if (image.imageOrientation == UIImageOrientationUp) { - return image; - } - UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat]; - format.scale = image.scale; UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:image.size format:format]; return [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; @@ -99,8 +94,27 @@ - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(con if (_cancelled) { return; } - _image = RCTUprightImage(image); - _onLoad(); + if (image.imageOrientation == UIImageOrientationUp) { + _image = image; + _onLoad(); + return; + } + + // Redrawing a full-size photo is slow, so do it off the main queue. + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat]; + format.scale = image.scale; + __weak RCTBackgroundImageURLEntry *weakSelf = self; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + UIImage *uprightImage = RCTUprightImage(image, format); + dispatch_async(dispatch_get_main_queue(), ^{ + RCTBackgroundImageURLEntry *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf->_cancelled) { + return; + } + strongSelf->_image = uprightImage; + strongSelf->_onLoad(); + }); + }); } - (void)didReceiveProgress:(float)progress diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h index d06ca170f6a1..dc16e1a80c39 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h @@ -13,6 +13,7 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea itemIntrinsicSize:(CGSize)itemIntrinsicSize + itemHasIntrinsicRatio:(BOOL)itemHasIntrinsicRatio backgroundSize:(const facebook::react::BackgroundSize &)backgroundSize backgroundRepeat:(const facebook::react::BackgroundRepeat &)backgroundRepeat; diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm index 278d53a226fd..f236e3153726 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm @@ -150,6 +150,7 @@ + (CALayer *)createBackgroundImageLayerWithSize:(const CGRect &)positioningArea + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea itemIntrinsicSize:(CGSize)itemIntrinsicSize + itemHasIntrinsicRatio:(BOOL)itemHasIntrinsicRatio backgroundSize:(const facebook::react::BackgroundSize &)backgroundSize backgroundRepeat:(const facebook::react::BackgroundRepeat &)backgroundRepeat { @@ -159,9 +160,13 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea CGSize itemFinalSize = itemIntrinsicSize; CGSize positioningAreaSize = positioningArea.size; + bool xAuto = true; + bool yAuto = true; if (std::holds_alternative(backgroundSize)) { auto backgroundSizeLengthPercentage = std::get(backgroundSize); + xAuto = backgroundSizeLengthPercentage.isXAuto(); + yAuto = backgroundSizeLengthPercentage.isYAuto(); if (std::holds_alternative(backgroundSizeLengthPercentage.x)) { auto xValue = std::get(backgroundSizeLengthPercentage.x); if (xValue.unit == facebook::react::UnitType::Percent) { @@ -181,6 +186,15 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea } } + // An `auto` dimension keeps the image's aspect ratio. Without a ratio (gradients), it stays at its intrinsic size. + bool keepsRatio = itemHasIntrinsicRatio && itemIntrinsicSize.width > 0 && itemIntrinsicSize.height > 0; + CGFloat ratio = keepsRatio ? itemIntrinsicSize.width / itemIntrinsicSize.height : 0; + if (keepsRatio && !xAuto && yAuto) { + itemFinalSize.height = itemFinalSize.width / ratio; + } else if (keepsRatio && xAuto && !yAuto) { + itemFinalSize.width = itemFinalSize.height * ratio; + } + if (backgroundRepeat.x == facebook::react::BackgroundRepeatStyle::Round) { if (itemFinalSize.width > 0 && positioningAreaSize.width > 0) { if (fmod(positioningAreaSize.width, itemFinalSize.width) != 0) { @@ -203,6 +217,15 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea } } + // With `round` on one axis only, an `auto` other axis is scaled again to restore the ratio. + bool roundX = backgroundRepeat.x == facebook::react::BackgroundRepeatStyle::Round; + bool roundY = backgroundRepeat.y == facebook::react::BackgroundRepeatStyle::Round; + if (keepsRatio && roundX && !roundY && yAuto) { + itemFinalSize.height = itemFinalSize.width / ratio; + } else if (keepsRatio && roundY && !roundX && xAuto) { + itemFinalSize.width = itemFinalSize.height * ratio; + } + return itemFinalSize; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index c3f665a384b2..f3987b1fc33a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -661,7 +661,7 @@ interface RCTBackgroundImageURLLoader : public NSObject { interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); - public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); + public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } interface RCTBlobManager : public NSObject { @@ -2073,7 +2073,7 @@ interface RCTViewComponentView : public UIView imageManager); + public virtual void setBackgroundImageManager:(std::shared_ptr imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index a41d348a4717..7daad871bc07 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -670,7 +670,7 @@ interface RCTBackgroundImageURLLoader : public NSObject { interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); - public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); + public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } interface RCTBlobManager : public NSObject { @@ -2131,7 +2131,7 @@ interface RCTViewComponentView : public UIView imageManager); + public virtual void setBackgroundImageManager:(std::shared_ptr imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index b3a25a02de8e..6bc0fac615d0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -661,7 +661,7 @@ interface RCTBackgroundImageURLLoader : public NSObject { interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); - public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); + public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } interface RCTBlobManager : public NSObject { @@ -2066,7 +2066,7 @@ interface RCTViewComponentView : public UIView imageManager); + public virtual void setBackgroundImageManager:(std::shared_ptr imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 1ed42e507907..119f016a4f94 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -661,7 +661,7 @@ interface RCTBackgroundImageURLLoader : public NSObject { interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); - public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); + public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } interface RCTBlobManager : public NSObject { @@ -2073,7 +2073,7 @@ interface RCTViewComponentView : public UIView imageManager); + public virtual void setBackgroundImageManager:(std::shared_ptr imageManager); public virtual void updateEventEmitter:(const facebook::react::EventEmitter::Shared& eventEmitter); public virtual void updateLayoutMetrics:oldLayoutMetrics:(const facebook::react::LayoutMetrics& layoutMetrics, const facebook::react::LayoutMetrics& oldLayoutMetrics); public virtual void updateProps:oldProps:(const facebook::react::Props::Shared& props, const facebook::react::Props::Shared& oldProps); From 746632f41509d7df647915569997e984c57f2f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 29 Oct 2026 16:32:16 +0530 Subject: [PATCH 5/9] Use at least one tile for background-repeat: round on iOS When the tile is more than twice the size of the box, round() gives 0 and the tile kept its size. CSS requires at least one tile, so the tile now shrinks to fit the box. This also applies to gradients. --- .../React/Fabric/Utils/RCTBackgroundImageUtils.mm | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm index f236e3153726..01b95a557ec0 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm @@ -198,10 +198,9 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea if (backgroundRepeat.x == facebook::react::BackgroundRepeatStyle::Round) { if (itemFinalSize.width > 0 && positioningAreaSize.width > 0) { if (fmod(positioningAreaSize.width, itemFinalSize.width) != 0) { - auto divisor = static_cast(round(positioningAreaSize.width / itemFinalSize.width)); - if (divisor > 0) { - itemFinalSize.width = positioningAreaSize.width / divisor; - } + // CSS rounds to the nearest whole number of tiles, but at least one. + CGFloat divisor = std::max(1, round(positioningAreaSize.width / itemFinalSize.width)); + itemFinalSize.width = positioningAreaSize.width / divisor; } } } @@ -209,10 +208,9 @@ + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea if (backgroundRepeat.y == facebook::react::BackgroundRepeatStyle::Round) { if (itemFinalSize.height > 0 && positioningAreaSize.height > 0) { if (fmod(positioningAreaSize.height, itemFinalSize.height) != 0) { - auto divisor = static_cast(round(positioningAreaSize.height / itemFinalSize.height)); - if (divisor > 0) { - itemFinalSize.height = positioningAreaSize.height / divisor; - } + // CSS rounds to the nearest whole number of tiles, but at least one. + CGFloat divisor = std::max(1, round(positioningAreaSize.height / itemFinalSize.height)); + itemFinalSize.height = positioningAreaSize.height / divisor; } } } From 25963f9460dce5664c5eafe6d1027b3ea303991f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 29 Oct 2026 16:51:39 +0530 Subject: [PATCH 6/9] Orient EXIF-rotated background url() images with a layer transform on iOS A layer draws the CGImage, which ignores imageOrientation. Instead of redrawing the whole image into a new bitmap, show the CGImage in a sublayer and rotate or mirror it with a transform. This removes the second bitmap and the background-queue redraw. --- .../View/RCTViewComponentView.mm | 4 +- .../Utils/RCTBackgroundImageURLLoader.mm | 32 +----------- .../Fabric/Utils/RCTBackgroundImageUtils.h | 3 ++ .../Fabric/Utils/RCTBackgroundImageUtils.mm | 51 +++++++++++++++++++ .../api-snapshots/ReactAppleDebugCxx.api | 1 + .../api-snapshots/ReactAppleFrameworksCxx.api | 1 + .../api-snapshots/ReactAppleNewarchCxx.api | 1 + .../api-snapshots/ReactAppleReleaseCxx.api | 1 + 8 files changed, 61 insertions(+), 33 deletions(-) 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 252412ab462a..c62ebb181868 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1458,9 +1458,7 @@ - (void)invalidateLayer const auto &radialGradient = std::get(backgroundImage); itemLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient]; } else if (urlImage != nil) { - itemLayer = [CALayer layer]; - itemLayer.frame = CGRectMake(0, 0, backgroundImageSize.width, backgroundImageSize.height); - itemLayer.contents = (__bridge id)urlImage.CGImage; + itemLayer = [RCTBackgroundImageUtils createURLImageLayerWithImage:urlImage size:backgroundImageSize]; } if (itemLayer != nil) { diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm index 4b63fee6bdd9..cae13bc29fd4 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm @@ -18,15 +18,6 @@ using namespace facebook::react; -// Layers draw the `CGImage`, which ignores `imageOrientation`. -static UIImage *RCTUprightImage(UIImage *image, UIGraphicsImageRendererFormat *format) -{ - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:image.size format:format]; - return [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { - [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; - }]; -} - /* * The image for one uri. It is the delegate of its own observer proxy. A * callback that is queued on the main queue retains it, so the callback @@ -94,27 +85,8 @@ - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(con if (_cancelled) { return; } - if (image.imageOrientation == UIImageOrientationUp) { - _image = image; - _onLoad(); - return; - } - - // Redrawing a full-size photo is slow, so do it off the main queue. - UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat]; - format.scale = image.scale; - __weak RCTBackgroundImageURLEntry *weakSelf = self; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ - UIImage *uprightImage = RCTUprightImage(image, format); - dispatch_async(dispatch_get_main_queue(), ^{ - RCTBackgroundImageURLEntry *strongSelf = weakSelf; - if (strongSelf == nil || strongSelf->_cancelled) { - return; - } - strongSelf->_image = uprightImage; - strongSelf->_onLoad(); - }); - }); + _image = image; + _onLoad(); } - (void)didReceiveProgress:(float)progress diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h index dc16e1a80c39..47c2a239628d 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.h @@ -8,6 +8,7 @@ #import #import #import +#import @interface RCTBackgroundImageUtils : NSObject @@ -24,4 +25,6 @@ backgroundRepeat:(const facebook::react::BackgroundRepeat &)backgroundRepeat itemLayer:(CALayer *)itemLayer; ++ (CALayer *)createURLImageLayerWithImage:(UIImage *)image size:(CGSize)size; + @end diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm index 01b95a557ec0..3cbc4ffbd40a 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageUtils.mm @@ -148,6 +148,57 @@ + (CALayer *)createBackgroundImageLayerWithSize:(const CGRect &)positioningArea return backgroundImageLayer; } +// Layers draw the `CGImage`, which ignores `imageOrientation`, so a transform orients it. ++ (CALayer *)createURLImageLayerWithImage:(UIImage *)image size:(CGSize)size +{ + CALayer *itemLayer = [CALayer layer]; + itemLayer.frame = CGRectMake(0, 0, size.width, size.height); + if (image.imageOrientation == UIImageOrientationUp) { + itemLayer.contents = (__bridge id)image.CGImage; + return itemLayer; + } + + CATransform3D transform = CATransform3DIdentity; + BOOL swapsAxes = NO; + switch (image.imageOrientation) { + case UIImageOrientationUp: + break; + case UIImageOrientationUpMirrored: + transform = CATransform3DMakeScale(-1, 1, 1); + break; + case UIImageOrientationDown: + transform = CATransform3DMakeRotation(M_PI, 0, 0, 1); + break; + case UIImageOrientationDownMirrored: + transform = CATransform3DMakeScale(1, -1, 1); + break; + case UIImageOrientationRight: + transform = CATransform3DMakeRotation(M_PI_2, 0, 0, 1); + swapsAxes = YES; + break; + case UIImageOrientationLeft: + transform = CATransform3DMakeRotation(-M_PI_2, 0, 0, 1); + swapsAxes = YES; + break; + case UIImageOrientationLeftMirrored: + transform = CATransform3DConcat(CATransform3DMakeRotation(M_PI_2, 0, 0, 1), CATransform3DMakeScale(-1, 1, 1)); + swapsAxes = YES; + break; + case UIImageOrientationRightMirrored: + transform = CATransform3DConcat(CATransform3DMakeRotation(-M_PI_2, 0, 0, 1), CATransform3DMakeScale(-1, 1, 1)); + swapsAxes = YES; + break; + } + + CALayer *imageLayer = [CALayer layer]; + imageLayer.contents = (__bridge id)image.CGImage; + imageLayer.bounds = swapsAxes ? CGRectMake(0, 0, size.height, size.width) : CGRectMake(0, 0, size.width, size.height); + imageLayer.position = CGPointMake(size.width / 2, size.height / 2); + imageLayer.transform = transform; + [itemLayer addSublayer:imageLayer]; + return itemLayer; +} + + (CGSize)calculateBackgroundImageSize:(const CGRect &)positioningArea itemIntrinsicSize:(CGSize)itemIntrinsicSize itemHasIntrinsicRatio:(BOOL)itemHasIntrinsicRatio diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index f3987b1fc33a..c906f8bdf833 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -661,6 +661,7 @@ interface RCTBackgroundImageURLLoader : public NSObject { interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); + public virtual static CALayer* createURLImageLayerWithImage:size:(UIImage* image, CGSize size); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index 7daad871bc07..1c18657fa447 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -670,6 +670,7 @@ interface RCTBackgroundImageURLLoader : public NSObject { interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); + public virtual static CALayer* createURLImageLayerWithImage:size:(UIImage* image, CGSize size); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 6bc0fac615d0..6b09da439ca1 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -661,6 +661,7 @@ interface RCTBackgroundImageURLLoader : public NSObject { interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); + public virtual static CALayer* createURLImageLayerWithImage:size:(UIImage* image, CGSize size); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 119f016a4f94..b5c8bf51a5c7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -661,6 +661,7 @@ interface RCTBackgroundImageURLLoader : public NSObject { interface RCTBackgroundImageUtils : public NSObject { public virtual static CALayer* createBackgroundImageLayerWithSize:paintingArea:itemSize:backgroundPosition:backgroundRepeat:itemLayer:(const CGRect& positioningArea, const CGRect& paintingArea, const CGSize& itemSize, const facebook::react::BackgroundPosition& backgroundPosition, const facebook::react::BackgroundRepeat& backgroundRepeat, CALayer* itemLayer); + public virtual static CALayer* createURLImageLayerWithImage:size:(UIImage* image, CGSize size); public virtual static CGSize calculateBackgroundImageSize:itemIntrinsicSize:itemHasIntrinsicRatio:backgroundSize:backgroundRepeat:(const CGRect& positioningArea, CGSize itemIntrinsicSize, BOOL itemHasIntrinsicRatio, const facebook::react::BackgroundSize& backgroundSize, const facebook::react::BackgroundRepeat& backgroundRepeat); } From 583d91a17a739e7ee66731536f18e79dd8914b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 29 Oct 2026 18:54:08 +0530 Subject: [PATCH 7/9] Use umbrella headers in RCTBackgroundImageURLLoader.h and reuse an existing ImageManager - The exported header now includes and , so it builds under RN_STRICT_API like the other React/Fabric headers. - RCTSurfacePresenter creates the shared ImageManager only when the context container does not have one yet. insert() keeps an existing value, so the extra ImageManager was created and thrown away. --- packages/react-native/React/Fabric/RCTSurfacePresenter.mm | 3 ++- .../React/Fabric/Utils/RCTBackgroundImageURLLoader.h | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index 683f99f07065..69f72135ccb2 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -100,7 +100,8 @@ - (instancetype)initWithContextContainer:(std::shared_ptr` and `url()` background images, because `RCTImageLoader` must not get // requests from more than one queue. Its constructor requires the image loader. - if (contextContainer->find>("RCTImageLoader").has_value()) { + if (!contextContainer->find>(ImageManagerKey).has_value() && + contextContainer->find>("RCTImageLoader").has_value()) { contextContainer->insert(ImageManagerKey, std::make_shared(contextContainer)); } diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h index 5a5808c8a1a1..23a903c5895e 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.h @@ -5,9 +5,9 @@ * LICENSE file in the root directory of this source tree. */ +#import +#import #import -#import -#import #include #include From b0e2697e6b6825ad35b49b5b6b4d5026467ad746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 29 Oct 2026 19:32:10 +0530 Subject: [PATCH 8/9] Remove two comments in RCTBackgroundImageURLLoader --- .../React/Fabric/Utils/RCTBackgroundImageURLLoader.mm | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm index cae13bc29fd4..4e062e54e15b 100644 --- a/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm +++ b/packages/react-native/React/Fabric/Utils/RCTBackgroundImageURLLoader.mm @@ -151,8 +151,6 @@ - (void)loadBackgroundImages:(const std::vector &)backgroundIma return; } - // Keep the entries for uris that are still used, so their images do not - // disappear while the new ones load. NSMutableArray *entries = [NSMutableArray arrayWithCapacity:uris.size()]; NSMutableArray *newEntries = [NSMutableArray new]; for (const auto &uri : uris) { @@ -170,8 +168,6 @@ - (void)loadBackgroundImages:(const std::vector &)backgroundIma } } - // Start only after `_entries` is updated, because a cached response can - // arrive synchronously and call `onLoad`. _entries = entries; for (RCTBackgroundImageURLEntry *entry in newEntries) { [entry startWithImageManager:*_imageManager surfaceId:surfaceId]; From ce03714ce55f7f0b5f24c4516d8a918fb93a980d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Thu, 29 Oct 2026 20:01:21 +0530 Subject: [PATCH 9/9] Add iOS unit tests for background url() images RCTBackgroundImageUtilsTests checks: - an auto size keeps the aspect ratio of a url image, but not of a gradient - background-repeat: round uses at least one tile - createURLImageLayerWithImage:size: draws all 8 EXIF orientations with the same pixels as UIImage --- .../RNTesterPods.xcodeproj/project.pbxproj | 4 + .../RCTBackgroundImageUtilsTests.mm | 139 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 packages/rn-tester/RNTesterUnitTests/RCTBackgroundImageUtilsTests.mm diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 1d336b424136..356cdec6bc71 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ E7DB20E622B2BAA6005AC45F /* RCTImageLoaderHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20C922B2BAA5005AC45F /* RCTImageLoaderHelpers.m */; }; E7DB20E722B2BAA6005AC45F /* RCTConvert_NSURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CA22B2BAA5005AC45F /* RCTConvert_NSURLTests.m */; }; E7DB20E822B2BAA6005AC45F /* RCTModuleMethodTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CB22B2BAA5005AC45F /* RCTModuleMethodTests.mm */; }; + B7C4E2A15D3F48096A1B2C02 /* RCTBackgroundImageUtilsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = B7C4E2A15D3F48096A1B2C01 /* RCTBackgroundImageUtilsTests.mm */; }; E7DB20E922B2BAA6005AC45F /* RCTComponentPropsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */; }; E7DB20EA22B2BAA6005AC45F /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CD22B2BAA5005AC45F /* RCTImageUtilTests.m */; }; E7DB20EB22B2BAA6005AC45F /* RCTConvert_YGValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CE22B2BAA5005AC45F /* RCTConvert_YGValueTests.m */; }; @@ -129,6 +130,7 @@ E7DB20C922B2BAA5005AC45F /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = ""; }; E7DB20CA22B2BAA5005AC45F /* RCTConvert_NSURLTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_NSURLTests.m; sourceTree = ""; }; E7DB20CB22B2BAA5005AC45F /* RCTModuleMethodTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTModuleMethodTests.mm; sourceTree = ""; }; + B7C4E2A15D3F48096A1B2C01 /* RCTBackgroundImageUtilsTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTBackgroundImageUtilsTests.mm; sourceTree = ""; }; E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTComponentPropsTests.m; sourceTree = ""; }; E7DB20CD22B2BAA5005AC45F /* RCTImageUtilTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtilTests.m; sourceTree = ""; }; E7DB20CE22B2BAA5005AC45F /* RCTConvert_YGValueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_YGValueTests.m; sourceTree = ""; }; @@ -322,6 +324,7 @@ E7DB20A322B2BA84005AC45F /* Info.plist */, E7DB20C622B2BAA5005AC45F /* RCTAllocationTests.m */, E7DB20B222B2BAA4005AC45F /* RCTAnimationUtilsTests.m */, + B7C4E2A15D3F48096A1B2C01 /* RCTBackgroundImageUtilsTests.mm */, E7DB20AB22B2BAA3005AC45F /* RCTBlobManagerTests.m */, E7DB20A922B2BAA3005AC45F /* RCTBundleURLProviderTests.m */, E7DB20CC22B2BAA5005AC45F /* RCTComponentPropsTests.m */, @@ -757,6 +760,7 @@ E7DB20EC22B2BAA6005AC45F /* RCTMultipartStreamReaderTests.m in Sources */, E7DB20E022B2BAA6005AC45F /* RCTMethodArgumentTests.m in Sources */, E7DB20E822B2BAA6005AC45F /* RCTModuleMethodTests.mm in Sources */, + B7C4E2A15D3F48096A1B2C02 /* RCTBackgroundImageUtilsTests.mm in Sources */, F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */, F1A0B1C23D4E5F6071829322 /* RCTTurboModuleStringConversionTests.mm in Sources */, E7DB20E222B2BAA6005AC45F /* RCTGzipTests.m in Sources */, diff --git a/packages/rn-tester/RNTesterUnitTests/RCTBackgroundImageUtilsTests.mm b/packages/rn-tester/RNTesterUnitTests/RCTBackgroundImageUtilsTests.mm new file mode 100644 index 000000000000..ce61ebd066fa --- /dev/null +++ b/packages/rn-tester/RNTesterUnitTests/RCTBackgroundImageUtilsTests.mm @@ -0,0 +1,139 @@ +/* + * 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 + +#include +#include +#include +#include + +using namespace facebook::react; + +static BackgroundSize RCTBackgroundSizeInPoints(std::optional width, std::optional height) +{ + BackgroundSizeLengthPercentage size; + if (width) { + size.x = ValueUnit(*width, UnitType::Point); + } + if (height) { + size.y = ValueUnit(*height, UnitType::Point); + } + return size; +} + +// A 60x40 image with four colored quadrants, so every rotation and mirror gives different pixels. +static CGImageRef RCTCreateQuadrantImage() +{ + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef context = + CGBitmapContextCreate(nullptr, 60, 40, 8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); + const CGFloat colors[4][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {1, 1, 0}}; + for (int i = 0; i < 4; i++) { + CGContextSetRGBFillColor(context, colors[i][0], colors[i][1], colors[i][2], 1); + CGContextFillRect(context, CGRectMake((i % 2) * 30, (i / 2) * 20, 30, 20)); + } + CGImageRef image = CGBitmapContextCreateImage(context); + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + return image; +} + +static std::vector RCTPixels(UIImage *image) +{ + CGImageRef cgImage = image.CGImage; + size_t width = CGImageGetWidth(cgImage); + size_t height = CGImageGetHeight(cgImage); + std::vector pixels(width * height * 4); + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef context = CGBitmapContextCreate( + pixels.data(), width, height, 8, width * 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); + return pixels; +} + +@interface RCTBackgroundImageUtilsTests : XCTestCase +@end + +@implementation RCTBackgroundImageUtilsTests + +- (void)testAutoHeightKeepsTheRatioOfAURLImage +{ + CGSize size = [RCTBackgroundImageUtils calculateBackgroundImageSize:CGRectMake(0, 0, 200, 200) + itemIntrinsicSize:CGSizeMake(300, 150) + itemHasIntrinsicRatio:YES + backgroundSize:RCTBackgroundSizeInPoints(100, std::nullopt) + backgroundRepeat:BackgroundRepeat{}]; + XCTAssertEqualWithAccuracy(size.width, 100, 0.001); + XCTAssertEqualWithAccuracy(size.height, 50, 0.001); +} + +- (void)testAutoHeightOfAGradientStaysAtItsIntrinsicSize +{ + CGSize size = [RCTBackgroundImageUtils calculateBackgroundImageSize:CGRectMake(0, 0, 200, 200) + itemIntrinsicSize:CGSizeMake(200, 200) + itemHasIntrinsicRatio:NO + backgroundSize:RCTBackgroundSizeInPoints(100, std::nullopt) + backgroundRepeat:BackgroundRepeat{}]; + XCTAssertEqualWithAccuracy(size.width, 100, 0.001); + XCTAssertEqualWithAccuracy(size.height, 200, 0.001); +} + +- (void)testRoundUsesAtLeastOneTile +{ + BackgroundRepeat repeat; + repeat.x = BackgroundRepeatStyle::Round; + repeat.y = BackgroundRepeatStyle::Round; + CGSize size = [RCTBackgroundImageUtils calculateBackgroundImageSize:CGRectMake(0, 0, 100, 100) + itemIntrinsicSize:CGSizeMake(100, 100) + itemHasIntrinsicRatio:NO + backgroundSize:RCTBackgroundSizeInPoints(250, 250) + backgroundRepeat:repeat]; + XCTAssertEqualWithAccuracy(size.width, 100, 0.001); + XCTAssertEqualWithAccuracy(size.height, 100, 0.001); +} + +- (void)testURLImageLayerDrawsEveryOrientationLikeUIImage +{ + CGImageRef cgImage = RCTCreateQuadrantImage(); + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat]; + format.scale = 1; + + for (NSInteger orientation = UIImageOrientationUp; orientation <= UIImageOrientationRightMirrored; orientation++) { + UIImage *image = [UIImage imageWithCGImage:cgImage scale:1 orientation:(UIImageOrientation)orientation]; + CGSize size = image.size; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:format]; + + UIImage *expected = [renderer imageWithActions:^(__unused UIGraphicsImageRendererContext *context) { + [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; + }]; + CALayer *layer = [RCTBackgroundImageUtils createURLImageLayerWithImage:image size:size]; + UIImage *actual = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { + [layer renderInContext:context.CGContext]; + }]; + + std::vector expectedPixels = RCTPixels(expected); + std::vector actualPixels = RCTPixels(actual); + XCTAssertEqual(expectedPixels.size(), actualPixels.size(), @"orientation %ld", (long)orientation); + size_t differentBytes = 0; + for (size_t i = 0; i < std::min(expectedPixels.size(), actualPixels.size()); i++) { + if (std::abs(expectedPixels[i] - actualPixels[i]) > 2) { + differentBytes++; + } + } + XCTAssertEqual(differentBytes, 0u, @"orientation %ld", (long)orientation); + } + + CGImageRelease(cgImage); +} + +@end