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)
963964static 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
969971BOOL 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+
10191130UIImage *__nullable RCTImageFromLocalBundleAssetURL (NSURL *imageURL)
10201131{
10211132 if (![imageURL.scheme isEqualToString: @" file" ]) {
@@ -1032,6 +1143,35 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL)
10321143
10331144UIImage *__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 ;
0 commit comments