Skip to content

Commit d123490

Browse files
url() support in background image on ios
1 parent c6b137c commit d123490

13 files changed

Lines changed: 440 additions & 19 deletions

File tree

‎packages/react-native/Package.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ let reactCore = RNTarget(
455455
let reactFabric = RNTarget(
456456
name: .reactFabric,
457457
path: "ReactCommon/react/renderer",
458+
searchPaths: ["ReactCommon/react/renderer/imagemanager/platform/ios"],
458459
excludedPaths: [
459460
"animated/tests",
460461
"animations/tests",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
#import <React/RCTImageResponseDelegate.h>
10+
#import <react/renderer/components/view/ViewShadowNode.h>
11+
12+
NS_ASSUME_NONNULL_BEGIN
13+
14+
@protocol RCTBackgroundImageURLLoaderDelegate <RCTImageResponseDelegate>
15+
16+
- (void)backgroundImagesDidLoad;
17+
18+
@end
19+
20+
@interface RCTBackgroundImageURLLoader : NSObject
21+
22+
@property (nonatomic, weak) id<RCTBackgroundImageURLLoaderDelegate> delegate;
23+
24+
- (void)updateStateWithNewState:(facebook::react::ViewShadowNode::ConcreteState::Shared)state oldState:(facebook::react::ViewShadowNode::ConcreteState::Shared)oldState;
25+
- (nullable UIImage *)loadedImageForUri:(NSString *)uri;
26+
- (void)reset;
27+
28+
@end
29+
30+
NS_ASSUME_NONNULL_END
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTBackgroundImageURLLoader.h"
9+
10+
#import <React/RCTImageResponseObserverProxy.h>
11+
#import <React/RCTLog.h>
12+
#import <react/renderer/components/view/ViewState.h>
13+
14+
#include <map>
15+
#include <memory>
16+
#include <string>
17+
18+
using namespace facebook::react;
19+
20+
@implementation RCTBackgroundImageURLLoader {
21+
ViewShadowNode::ConcreteState::Shared _state;
22+
std::map<std::string, std::shared_ptr<RCTImageResponseObserverProxy>> _uriToObserver;
23+
NSMutableDictionary<NSString *, UIImage *> *_loadedImages;
24+
NSMutableSet<NSString *> *_completedUris;
25+
}
26+
27+
- (instancetype)init
28+
{
29+
if (self = [super init]) {
30+
_loadedImages = [NSMutableDictionary new];
31+
_completedUris = [NSMutableSet new];
32+
}
33+
return self;
34+
}
35+
36+
- (void)updateStateWithNewState:(ViewShadowNode::ConcreteState::Shared)state
37+
oldState:(ViewShadowNode::ConcreteState::Shared)oldState
38+
{
39+
const auto *oldRequests = oldState ? &oldState->getData().getBackgroundImageRequests() : nullptr;
40+
const auto *newRequests = state ? &state->getData().getBackgroundImageRequests() : nullptr;
41+
42+
if (oldRequests && newRequests && *oldRequests == *newRequests) {
43+
return;
44+
}
45+
46+
if (oldRequests) {
47+
for (const auto &request : *oldRequests) {
48+
if (request.imageRequest) {
49+
auto it = _uriToObserver.find(request.imageSource.uri);
50+
if (it != _uriToObserver.end()) {
51+
auto &observerCoordinator = request.imageRequest->getObserverCoordinator();
52+
observerCoordinator.removeObserver(it->second);
53+
}
54+
}
55+
}
56+
}
57+
58+
_state = state;
59+
_uriToObserver.clear();
60+
[_loadedImages removeAllObjects];
61+
[_completedUris removeAllObjects];
62+
63+
if (newRequests) {
64+
// `addObserver` replays a cached image synchronously, so register every uri
65+
// first. Otherwise the completion check in notifyDelegateIfAllImagesLoaded
66+
// runs against a half-filled map.
67+
for (const auto &request : *newRequests) {
68+
if (request.imageRequest) {
69+
_uriToObserver.try_emplace(request.imageSource.uri);
70+
}
71+
}
72+
73+
for (const auto &request : *newRequests) {
74+
if (request.imageRequest) {
75+
auto it = _uriToObserver.find(request.imageSource.uri);
76+
// A null entry is a uri that has not been attached yet. Duplicated uris
77+
// share a single observer, matching the previous behaviour.
78+
if (it != _uriToObserver.end() && it->second == nullptr) {
79+
it->second = std::make_shared<RCTImageResponseObserverProxy>(self);
80+
auto &observerCoordinator = request.imageRequest->getObserverCoordinator();
81+
observerCoordinator.addObserver(it->second);
82+
}
83+
}
84+
}
85+
}
86+
}
87+
88+
- (UIImage *)loadedImageForUri:(NSString *)uri
89+
{
90+
return _loadedImages[uri];
91+
}
92+
93+
- (void)reset
94+
{
95+
if (_state) {
96+
const auto &requests = _state->getData().getBackgroundImageRequests();
97+
for (const auto &request : requests) {
98+
if (request.imageRequest) {
99+
auto it = _uriToObserver.find(request.imageSource.uri);
100+
if (it != _uriToObserver.end()) {
101+
auto &observerCoordinator = request.imageRequest->getObserverCoordinator();
102+
observerCoordinator.removeObserver(it->second);
103+
}
104+
}
105+
}
106+
}
107+
108+
_state = nullptr;
109+
_uriToObserver.clear();
110+
[_loadedImages removeAllObjects];
111+
[_completedUris removeAllObjects];
112+
}
113+
114+
#pragma mark - RCTImageResponseDelegate
115+
116+
- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer
117+
{
118+
for (const auto &[uri, observerProxy] : _uriToObserver) {
119+
if (observerProxy.get() == observer) {
120+
NSString *nsUri = [NSString stringWithUTF8String:uri.c_str()];
121+
_loadedImages[nsUri] = image;
122+
[_completedUris addObject:nsUri];
123+
break;
124+
}
125+
}
126+
127+
[self notifyDelegateIfAllImagesLoaded];
128+
}
129+
130+
- (void)didReceiveProgress:(float)progress
131+
loaded:(int64_t)loaded
132+
total:(int64_t)total
133+
fromObserver:(const void *)observer
134+
{
135+
// Progress tracking not needed for background images
136+
}
137+
138+
- (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer
139+
{
140+
for (const auto &[uri, observerProxy] : _uriToObserver) {
141+
if (observerProxy.get() == observer) {
142+
NSString *nsUri = [NSString stringWithUTF8String:uri.c_str()];
143+
RCTLogWarn(@"Failed to load background image: %@ - %@", nsUri, error);
144+
[_completedUris addObject:nsUri];
145+
break;
146+
}
147+
}
148+
149+
[self notifyDelegateIfAllImagesLoaded];
150+
}
151+
152+
- (void)notifyDelegateIfAllImagesLoaded
153+
{
154+
if (_completedUris.count == _uriToObserver.size()) {
155+
[_delegate backgroundImagesDidLoad];
156+
}
157+
}
158+
159+
@end

‎packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
#import <react/renderer/core/LayoutMetrics.h>
1818
#import <react/renderer/core/Props.h>
1919

20+
#import "RCTBackgroundImageURLLoader.h"
21+
2022
NS_ASSUME_NONNULL_BEGIN
2123

2224
/**
2325
* UIView class for <View> component.
2426
*/
25-
@interface RCTViewComponentView : UIView <RCTComponentViewProtocol, RCTTouchableComponentViewProtocol> {
27+
@interface RCTViewComponentView : UIView <RCTComponentViewProtocol, RCTTouchableComponentViewProtocol, RCTBackgroundImageURLLoaderDelegate> {
2628
@protected
2729
facebook::react::LayoutMetrics _layoutMetrics;
2830
facebook::react::SharedViewProps _props;

‎packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm‎

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ @implementation RCTViewComponentView {
122122
NSMutableSet<NSString *> *_accessibilityOrderNativeIDs;
123123
RCTSwiftUIContainerViewWrapper *_swiftUIWrapper;
124124
BOOL _focusable;
125+
RCTBackgroundImageURLLoader *_backgroundImageLoader;
125126
}
126127

127128
#ifdef RCT_DYNAMIC_FRAMEWORKS
@@ -136,6 +137,8 @@ - (instancetype)initWithFrame:(CGRect)frame
136137
if (self = [super initWithFrame:frame]) {
137138
_props = ViewShadowNode::defaultSharedProps();
138139
_reactSubviews = [NSMutableArray new];
140+
_backgroundImageLoader = [RCTBackgroundImageURLLoader new];
141+
_backgroundImageLoader.delegate = self;
139142
#if !TARGET_OS_TV
140143
self.multipleTouchEnabled = YES;
141144
#endif
@@ -675,6 +678,13 @@ - (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
675678
_eventEmitter = std::static_pointer_cast<const ViewEventEmitter>(eventEmitter);
676679
}
677680

681+
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
682+
{
683+
auto newViewState = std::static_pointer_cast<const ViewShadowNode::ConcreteState>(state);
684+
auto oldViewState = oldState ? std::static_pointer_cast<const ViewShadowNode::ConcreteState>(oldState) : nullptr;
685+
[_backgroundImageLoader updateStateWithNewState:newViewState oldState:oldViewState];
686+
}
687+
678688
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
679689
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
680690
{
@@ -775,6 +785,9 @@ - (void)prepareForRecycle
775785
_filterLayer = nil;
776786
[self clearExistingBackgroundImageLayers];
777787

788+
// Clean up background image observers
789+
[_backgroundImageLoader reset];
790+
778791
_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil;
779792
_eventEmitter.reset();
780793
_isJSResponder = NO;
@@ -1310,29 +1323,55 @@ - (void)invalidateLayer
13101323
backgroundRepeat = _props->backgroundRepeat[imageIndex % _props->backgroundRepeat.size()];
13111324
}
13121325

1313-
CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea
1314-
itemIntrinsicSize:backgroundPositioningArea.size
1315-
backgroundSize:backgroundSize
1316-
backgroundRepeat:backgroundRepeat];
1317-
1318-
CALayer *gradientLayer;
1326+
CALayer *itemLayer = nil;
13191327

13201328
if (std::holds_alternative<LinearGradient>(backgroundImage)) {
1329+
CGSize backgroundImageSize =
1330+
[RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea
1331+
itemIntrinsicSize:backgroundPositioningArea.size
1332+
backgroundSize:backgroundSize
1333+
backgroundRepeat:backgroundRepeat];
13211334
const auto &linearGradient = std::get<LinearGradient>(backgroundImage);
1322-
gradientLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient];
1335+
itemLayer = [RCTLinearGradient gradientLayerWithSize:backgroundImageSize gradient:linearGradient];
13231336
} else if (std::holds_alternative<RadialGradient>(backgroundImage)) {
1337+
CGSize backgroundImageSize =
1338+
[RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea
1339+
itemIntrinsicSize:backgroundPositioningArea.size
1340+
backgroundSize:backgroundSize
1341+
backgroundRepeat:backgroundRepeat];
13241342
const auto &radialGradient = std::get<RadialGradient>(backgroundImage);
1325-
gradientLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient];
1343+
itemLayer = [RCTRadialGradient gradientLayerWithSize:backgroundImageSize gradient:radialGradient];
1344+
} else if (std::holds_alternative<URLBackgroundImage>(backgroundImage)) {
1345+
const auto &urlBgImage = std::get<URLBackgroundImage>(backgroundImage);
1346+
NSString *uri = [NSString stringWithUTF8String:urlBgImage.uri.c_str()];
1347+
UIImage *loadedImage = [_backgroundImageLoader loadedImageForUri:uri];
1348+
if (loadedImage != nil) {
1349+
// Bundled assets carry their natural size in layout units. A plain url
1350+
// has none, so fall back to the decoded image.
1351+
CGSize intrinsicSize = (urlBgImage.intrinsicWidth > 0 && urlBgImage.intrinsicHeight > 0)
1352+
? CGSizeMake(urlBgImage.intrinsicWidth, urlBgImage.intrinsicHeight)
1353+
: loadedImage.size;
1354+
CGSize backgroundImageSize = [RCTBackgroundImageUtils calculateBackgroundImageSize:backgroundPositioningArea
1355+
itemIntrinsicSize:intrinsicSize
1356+
backgroundSize:backgroundSize
1357+
backgroundRepeat:backgroundRepeat];
1358+
CALayer *imageLayer = [CALayer layer];
1359+
imageLayer.frame = CGRectMake(0, 0, backgroundImageSize.width, backgroundImageSize.height);
1360+
imageLayer.contents = (__bridge id)loadedImage.CGImage;
1361+
imageLayer.contentsGravity = kCAGravityResizeAspectFill;
1362+
itemLayer = imageLayer;
1363+
}
13261364
}
13271365

1328-
if (gradientLayer != nil) {
1366+
if (itemLayer != nil) {
1367+
CGSize itemSize = itemLayer.frame.size;
13291368
CALayer *backgroundImageLayer =
13301369
[RCTBackgroundImageUtils createBackgroundImageLayerWithSize:backgroundPositioningArea
13311370
paintingArea:backgroundPaintingArea
1332-
itemSize:backgroundImageSize
1371+
itemSize:itemSize
13331372
backgroundPosition:backgroundPosition
13341373
backgroundRepeat:backgroundRepeat
1335-
itemLayer:gradientLayer];
1374+
itemLayer:itemLayer];
13361375
[self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetricsBI];
13371376
backgroundImageLayer.masksToBounds = YES;
13381377
backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
@@ -1446,6 +1485,13 @@ - (void)clearExistingBackgroundImageLayers
14461485
[_backgroundImageLayers removeAllObjects];
14471486
}
14481487

1488+
#pragma mark - RCTBackgroundImageURLLoaderDelegate
1489+
1490+
- (void)backgroundImagesDidLoad
1491+
{
1492+
[self invalidateLayer];
1493+
}
1494+
14491495
#pragma mark - Accessibility
14501496

14511497
- (NSObject *)accessibilityElement

‎packages/react-native/ReactCommon/React-Fabric.podspec‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ Pod::Spec.new do |s|
149149
sss.dependency "Yoga"
150150
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}"])
151151
sss.header_dir = "react/renderer/components/view"
152+
sss.pod_target_xcconfig = {
153+
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/react/renderer/imagemanager/platform/ios\""
154+
}
152155
end
153156

154157
ss.subspec "viewUmbrella" do |sss|

‎packages/react-native/ReactCommon/react/renderer/components/view/CMakeLists.txt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ add_library(rrc_view OBJECT ${rrc_view_SRC})
2222
react_native_android_selector(platform_DIR
2323
${CMAKE_CURRENT_SOURCE_DIR}/platform/android/
2424
${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/)
25-
target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR})
25+
react_native_android_selector(imagemanager_platform_DIR
26+
${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/android/
27+
${REACT_COMMON_DIR}/react/renderer/imagemanager/platform/cxx/)
28+
target_include_directories(rrc_view PUBLIC ${REACT_COMMON_DIR} ${platform_DIR} ${imagemanager_platform_DIR})
2629

2730
target_link_libraries(rrc_view
2831
folly_runtime
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "ViewComponentDescriptor.h"
9+
#include <react/renderer/imagemanager/ImageManager.h>
10+
11+
namespace facebook::react {
12+
13+
extern const char ImageManagerKey[];
14+
15+
ViewComponentDescriptor::ViewComponentDescriptor(
16+
const ComponentDescriptorParameters& parameters)
17+
: ConcreteComponentDescriptor(parameters),
18+
imageManager_(
19+
getManagerByName<ImageManager>(contextContainer_, ImageManagerKey)) {}
20+
21+
void ViewComponentDescriptor::adopt(ShadowNode& shadowNode) const {
22+
ConcreteComponentDescriptor::adopt(shadowNode);
23+
24+
auto& viewShadowNode = static_cast<ViewShadowNode&>(shadowNode);
25+
viewShadowNode.setImageManager(imageManager_);
26+
}
27+
28+
} // namespace facebook::react

0 commit comments

Comments
 (0)