Skip to content

Commit 6409517

Browse files
authored
Merge branch 'main' into feat/scene-delegate
2 parents 5453a82 + 50a35db commit 6409517

19 files changed

Lines changed: 444 additions & 67 deletions

File tree

packages/react-native/React/Base/RCTUtils.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ RCT_EXTERN NSData *__nullable RCTDecompressGzipData(NSData *__nullable data, NSU
148148
// (or nil, if the URL does not specify a path within the main bundle)
149149
RCT_EXTERN NSString *__nullable RCTBundlePathForURL(NSURL *__nullable URL);
150150

151+
// Returns the asset catalog image name for a packager asset URL, or nil if the
152+
// URL is not a main-bundle packager asset. The name matches the identifier the
153+
// CLI uses when generating the catalog (see assetPathUtils.getResourceIdentifier).
154+
RCT_EXTERN NSString *__nullable RCTAssetCatalogNameForURL(NSURL *__nullable URL);
155+
151156
// Returns the Path of Library directory
152157
RCT_EXTERN NSString *__nullable RCTLibraryPath(void);
153158

packages/react-native/React/Base/RCTUtils.mm

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import <objc/runtime.h>
1414
#import <zlib.h>
1515
#import <atomic>
16+
#import <vector>
1617

1718
#import <UIKit/UIKit.h>
1819

@@ -963,7 +964,8 @@ BOOL RCTIsGzippedData(NSData *__nullable data)
963964
static BOOL RCTIsImageAssetsPath(NSString *path)
964965
{
965966
NSString *extension = [path pathExtension];
966-
return [extension isEqualToString:@"png"] || [extension isEqualToString:@"jpg"];
967+
return
968+
[extension isEqualToString:@"png"] || [extension isEqualToString:@"jpg"] || [extension isEqualToString:@"jpeg"];
967969
}
968970

969971
BOOL RCTIsBundleAssetURL(NSURL *__nullable imageURL)
@@ -1016,6 +1018,115 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL)
10161018
return bundleCache[key];
10171019
}
10181020

1021+
static BOOL RCTUseAssetCatalog(void)
1022+
{
1023+
static BOOL useAssetCatalog = NO;
1024+
static dispatch_once_t onceToken;
1025+
dispatch_once(&onceToken, ^{
1026+
useAssetCatalog = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTUseAssetCatalog"] boolValue];
1027+
});
1028+
return useAssetCatalog;
1029+
}
1030+
1031+
// The bundle react-native-xcode.sh compiles packager image assets into
1032+
// (RNAssets.bundle, an actool-compiled asset catalog inside the app).
1033+
static NSBundle *__nullable RCTAssetCatalogBundle(void)
1034+
{
1035+
static NSBundle *bundle;
1036+
static dispatch_once_t onceToken;
1037+
dispatch_once(&onceToken, ^{
1038+
NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:@"RNAssets" withExtension:@"bundle"];
1039+
if (bundleURL != nil) {
1040+
bundle = [NSBundle bundleWithURL:bundleURL];
1041+
}
1042+
});
1043+
return bundle;
1044+
}
1045+
1046+
NSString *__nullable RCTAssetCatalogNameForURL(NSURL *__nullable URL)
1047+
{
1048+
// The "assets/" prefix the packager uses for all image assets. The CLI strips
1049+
// it from the identifiers it names the imagesets with.
1050+
constexpr NSUInteger assetsPrefixLength = sizeof("assets/") - 1;
1051+
1052+
NSString *path = RCTBundlePathForURL(URL);
1053+
// Packager assets always live under "assets/". Anything else (sub-bundles,
1054+
// CodePush/OTA assets outside the main bundle) is not in the catalog.
1055+
if (path == nil || ![path hasPrefix:@"assets/"]) {
1056+
return nil;
1057+
}
1058+
1059+
// Other packager assets (gif, webp, ...) are copied as plain files and must
1060+
// use the regular loader.
1061+
if (!RCTIsImageAssetsPath(path)) {
1062+
return nil;
1063+
}
1064+
1065+
// File system paths come back decomposed (NFD); restore the precomposed form
1066+
// the packager saw on disk so non-ASCII characters filter out the same way
1067+
// they do in the CLI's identifier.
1068+
path = path.precomposedStringWithCanonicalMapping;
1069+
1070+
const NSUInteger length = path.length;
1071+
unichar stackBuffer[256];
1072+
std::vector<unichar> heapBuffer;
1073+
unichar *chars = stackBuffer;
1074+
if (length > 256) {
1075+
heapBuffer.resize(length);
1076+
chars = heapBuffer.data();
1077+
}
1078+
[path getCharacters:chars range:NSMakeRange(0, length)];
1079+
1080+
// Strip the file extension (guaranteed present by RCTIsImageAssetsPath) and
1081+
// an optional "@<scale>x" suffix (integer or fractional, e.g. "@2x",
1082+
// "@1.5x"). The catalog stores a single imageset per image and resolves the
1083+
// scale by name at runtime, see
1084+
// https://developer.apple.com/documentation/xcode/managing-assets-with-asset-catalogs
1085+
NSUInteger end = length - 1;
1086+
while (end > assetsPrefixLength && chars[end] != '.') {
1087+
end--;
1088+
}
1089+
if (end > assetsPrefixLength && chars[end - 1] == 'x') {
1090+
// Walk back over "@<digits>(.<digits>)?" ending at the "x".
1091+
NSUInteger cursor = end - 1;
1092+
while (cursor > assetsPrefixLength && chars[cursor - 1] >= '0' && chars[cursor - 1] <= '9') {
1093+
cursor--;
1094+
}
1095+
if (cursor < end - 1) {
1096+
if (chars[cursor - 1] == '@') {
1097+
end = cursor - 1;
1098+
} else if (chars[cursor - 1] == '.') {
1099+
NSUInteger integerPart = cursor - 1;
1100+
while (integerPart > assetsPrefixLength && chars[integerPart - 1] >= '0' && chars[integerPart - 1] <= '9') {
1101+
integerPart--;
1102+
}
1103+
if (integerPart < cursor - 1 && chars[integerPart - 1] == '@') {
1104+
end = integerPart - 1;
1105+
}
1106+
}
1107+
}
1108+
}
1109+
1110+
// Build the identifier in place in a single pass: skip the "assets/" prefix,
1111+
// lowercase, encode the folder structure with "_" and drop anything that is
1112+
// not a valid identifier character, producing the same identifier as
1113+
// getResourceIdentifier in the CLI, which names the imagesets.
1114+
NSUInteger resultLength = 0;
1115+
for (NSUInteger i = assetsPrefixLength; i < end; i++) {
1116+
unichar c = chars[i];
1117+
if (c == '/') {
1118+
chars[resultLength++] = '_';
1119+
} else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') {
1120+
chars[resultLength++] = c;
1121+
} else if (c >= 'A' && c <= 'Z') {
1122+
chars[resultLength++] = c + ('a' - 'A');
1123+
}
1124+
}
1125+
1126+
NSString *name = [NSString stringWithCharacters:chars length:resultLength];
1127+
return name;
1128+
}
1129+
10191130
UIImage *__nullable RCTImageFromLocalBundleAssetURL(NSURL *imageURL)
10201131
{
10211132
if (![imageURL.scheme isEqualToString:@"file"]) {
@@ -1032,6 +1143,35 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL)
10321143

10331144
UIImage *__nullable RCTImageFromLocalAssetURL(NSURL *imageURL)
10341145
{
1146+
if (RCTUseAssetCatalog()) {
1147+
NSString *catalogName = RCTAssetCatalogNameForURL(imageURL);
1148+
if (catalogName != nil) {
1149+
// The app opted into the asset catalog and this is a packager asset, so it
1150+
// was compiled into RNAssets.bundle at build time. Trust the catalog and
1151+
// return directly, keeping the common path a single lookup with no
1152+
// filesystem fallback. Non-catalog assets (nil name) fall through below.
1153+
NSBundle *assetCatalogBundle = RCTAssetCatalogBundle();
1154+
if (assetCatalogBundle == nil) {
1155+
// Passing a nil bundle to imageNamed:inBundle: would silently search the
1156+
// main bundle instead, potentially resolving an unrelated app image.
1157+
RCTLogError(
1158+
@"RCTUseAssetCatalog is enabled but RNAssets.bundle was not found in the app. Image assets must be "
1159+
"bundled by react-native-xcode.sh, which compiles them into the app at build time. (loading %@)",
1160+
imageURL);
1161+
return nil;
1162+
}
1163+
UIImage *image = [UIImage imageNamed:catalogName inBundle:assetCatalogBundle compatibleWithTraitCollection:nil];
1164+
if (image == nil) {
1165+
RCTLogError(
1166+
@"Image \"%@\" (%@) was not found in the asset catalog. RCTUseAssetCatalog is enabled, "
1167+
"so image assets must be compiled into the app's RNAssets.bundle at build time.",
1168+
catalogName,
1169+
imageURL);
1170+
}
1171+
return image;
1172+
}
1173+
}
1174+
10351175
NSString *imageName = RCTBundlePathForURL(imageURL);
10361176

10371177
NSBundle *bundle = nil;

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Overflow.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@ internal enum class Overflow {
2525
/**
2626
* Parses a string into an Overflow value.
2727
*
28-
* @param overflow The string value (case-insensitive)
29-
* @return The corresponding Overflow, or null if not recognized
28+
* @param overflow The string value (case-insensitive), or null
29+
* @param default The value to return when [overflow] is null or unrecognized
30+
* @return The corresponding Overflow, or [default]
3031
*/
3132
@JvmStatic
32-
fun fromString(overflow: String): Overflow? {
33-
return when (overflow.lowercase()) {
34-
"visible" -> VISIBLE
35-
"hidden" -> HIDDEN
36-
"scroll" -> SCROLL
37-
else -> null
38-
}
39-
}
33+
@JvmOverloads
34+
fun fromString(overflow: String?, default: Overflow = VISIBLE): Overflow =
35+
when (overflow?.lowercase()) {
36+
"visible" -> VISIBLE
37+
"hidden" -> HIDDEN
38+
"scroll" -> SCROLL
39+
else -> default
40+
}
4041
}
4142
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,10 +419,11 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
419419
if (overflow == null) {
420420
Overflow.SCROLL
421421
} else {
422-
Overflow.fromString(overflow)
423-
?: if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid())
424-
Overflow.VISIBLE
425-
else Overflow.SCROLL
422+
Overflow.fromString(
423+
overflow,
424+
if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) Overflow.VISIBLE
425+
else Overflow.SCROLL,
426+
)
426427
}
427428
invalidate()
428429
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<cf60b52e15df339a179f392723007fab>>
7+
* @generated SignedSource<<1ad8f84ac759d8d225ce0fd57dccea7b>>
88
*/
99

1010
/**
@@ -387,10 +387,11 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
387387
if (overflow == null) {
388388
Overflow.SCROLL
389389
} else {
390-
Overflow.fromString(overflow)
391-
?: if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid())
392-
Overflow.VISIBLE
393-
else Overflow.SCROLL
390+
Overflow.fromString(
391+
overflow,
392+
if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) Overflow.VISIBLE
393+
else Overflow.SCROLL,
394+
)
394395
}
395396
invalidate()
396397
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<d321b1f5f7a34b5717cb7ef5300dda96>>
7+
* @generated SignedSource<<543b6175d2ce5e4ec68f1625f0c04095>>
88
*/
99

1010
/**
@@ -450,17 +450,15 @@ constructor(private val fpsListener: FpsListener? = null) :
450450
public companion object {
451451
public const val REACT_CLASS: String = "RCTScrollView"
452452

453-
public fun createExportedCustomDirectEventTypeConstants(): Map<String, Any> =
454-
mapOf(
455-
getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"),
456-
getJSEventName(ScrollEventType.BEGIN_DRAG) to
457-
mapOf("registrationName" to "onScrollBeginDrag"),
458-
getJSEventName(ScrollEventType.END_DRAG) to
459-
mapOf("registrationName" to "onScrollEndDrag"),
460-
getJSEventName(ScrollEventType.MOMENTUM_BEGIN) to
461-
mapOf("registrationName" to "onMomentumScrollBegin"),
462-
getJSEventName(ScrollEventType.MOMENTUM_END) to
463-
mapOf("registrationName" to "onMomentumScrollEnd"),
464-
)
453+
public fun createExportedCustomDirectEventTypeConstants(): Map<String, Any> = mapOf(
454+
getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"),
455+
getJSEventName(ScrollEventType.BEGIN_DRAG) to
456+
mapOf("registrationName" to "onScrollBeginDrag"),
457+
getJSEventName(ScrollEventType.END_DRAG) to mapOf("registrationName" to "onScrollEndDrag"),
458+
getJSEventName(ScrollEventType.MOMENTUM_BEGIN) to
459+
mapOf("registrationName" to "onMomentumScrollBegin"),
460+
getJSEventName(ScrollEventType.MOMENTUM_END) to
461+
mapOf("registrationName" to "onMomentumScrollEnd"),
462+
)
465463
}
466464
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,10 +379,11 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
379379
if (overflow == null) {
380380
Overflow.SCROLL
381381
} else {
382-
Overflow.fromString(overflow)
383-
?: if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid())
384-
Overflow.VISIBLE
385-
else Overflow.SCROLL
382+
Overflow.fromString(
383+
overflow,
384+
if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) Overflow.VISIBLE
385+
else Overflow.SCROLL,
386+
)
386387
}
387388
invalidate()
388389
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ internal class PreparedLayoutTextViewManager :
108108

109109
@ReactProp(name = "overflow")
110110
fun setOverflow(view: PreparedLayoutTextView, overflow: String?): Unit {
111-
view.overflow = overflow?.let { Overflow.fromString(it) } ?: Overflow.VISIBLE
111+
view.overflow = Overflow.fromString(overflow)
112112
}
113113

114114
@ReactProp(name = "accessible")

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -673,13 +673,7 @@ private void applyTextAttributes() {
673673
}
674674

675675
public void setOverflow(@Nullable String overflow) {
676-
if (overflow == null) {
677-
mOverflow = Overflow.VISIBLE;
678-
} else {
679-
@Nullable Overflow parsedOverflow = Overflow.fromString(overflow);
680-
mOverflow = parsedOverflow == null ? Overflow.VISIBLE : parsedOverflow;
681-
}
682-
676+
mOverflow = Overflow.fromString(overflow);
683677
invalidate();
684678
}
685679
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,13 +1185,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
11851185
}
11861186

11871187
public fun setOverflow(overflow: String?) {
1188-
if (overflow == null) {
1189-
this.overflow = Overflow.VISIBLE
1190-
} else {
1191-
val parsedOverflow = Overflow.fromString(overflow)
1192-
this.overflow = parsedOverflow ?: Overflow.VISIBLE
1193-
}
1194-
1188+
this.overflow = Overflow.fromString(overflow)
11951189
invalidate()
11961190
}
11971191

0 commit comments

Comments
 (0)