From 6f971b9f0baad9c47cc012b7842ffabe158f9df9 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Wed, 7 Oct 2020 17:13:13 -0400 Subject: [PATCH] Use asset catalog for ios images --- .gitignore | 3 + packages/react-native/React/Base/RCTUtils.h | 3 + packages/react-native/React/Base/RCTUtils.m | 67 +++++++++++++++++++ .../scripts/react-native-xcode.sh | 5 ++ packages/react-native/template/_gitignore | 3 + .../ios/HelloWorld.xcodeproj/project.pbxproj | 8 ++- .../RNTester/RNAssets.xcassets/Contents.json | 6 ++ .../RNTesterPods.xcodeproj/project.pbxproj | 7 +- .../RNTesterUnitTests/RCTURLUtilsTests.m | 21 ++++++ .../RNAssets.xcassets/Contents.json | 6 ++ 10 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 packages/rn-tester/RNTester/RNAssets.xcassets/Contents.json create mode 100644 template/ios/HelloWorld/RNAssets.xcassets/Contents.json diff --git a/.gitignore b/.gitignore index 85ddd447dda7a1..a9ee9f8e84365b 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,9 @@ package-lock.json !/packages/rn-tester/Pods/__offline_mirrors_hermes__ !/packages/rn-tester/Pods/__offline_mirrors_jsc__ +# Generated asset catalog +/packages/rn-tester/RNTester/RNAssets.xcassets/*.imageset + # @react-native/codegen /packages/react-native/React/FBReactNativeSpec/FBReactNativeSpec /packages/react-native-codegen/lib diff --git a/packages/react-native/React/Base/RCTUtils.h b/packages/react-native/React/Base/RCTUtils.h index dc218c47f199fc..bcef909366920f 100644 --- a/packages/react-native/React/Base/RCTUtils.h +++ b/packages/react-native/React/Base/RCTUtils.h @@ -135,6 +135,9 @@ RCT_EXTERN NSString *__nullable RCTLibraryPath(void); // (or nil, if the URL does not specify a path within the Library directory) RCT_EXTERN NSString *__nullable RCTLibraryPathForURL(NSURL *__nullable URL); +// Return the name of the asset in the catalog for a packager URL. +RCT_EXTERN NSString *__nullable RCTAssetCatalogNameForURL(NSURL *__nullable URL); + // Determines if a given image URL refers to a image in bundle RCT_EXTERN BOOL RCTIsBundleAssetURL(NSURL *__nullable imageURL); diff --git a/packages/react-native/React/Base/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m index 01bd5cde9fc3df..5fd18ec7814f2b 100644 --- a/packages/react-native/React/Base/RCTUtils.m +++ b/packages/react-native/React/Base/RCTUtils.m @@ -728,6 +728,62 @@ BOOL RCTIsGzippedData(NSData *__nullable data) return RCTRelativePathForURL(RCTHomePath(), URL); } +static NSRegularExpression *RCTAssetURLScaleRegex() +{ + static dispatch_once_t onceToken; + static NSRegularExpression *regex; + dispatch_once(&onceToken, ^{ + regex = [NSRegularExpression regularExpressionWithPattern:@"@\\dx$" options:0 error:nil]; + }); + return regex; +} + +static NSRegularExpression *RCTAssetURLCharactersRegex() +{ + static dispatch_once_t onceToken; + static NSRegularExpression *regex; + dispatch_once(&onceToken, ^{ + regex = [NSRegularExpression regularExpressionWithPattern:@"[^a-z0-9_]" options:0 error:nil]; + }); + return regex; +} + +NSString *__nullable RCTAssetCatalogNameForURL(NSURL *__nullable URL) +{ + NSString *path = RCTBundlePathForURL(URL); + // Packager assets always start with assets/ + if (path == nil || ![path hasPrefix:@"assets/"]) { + return nil; + } + + // Remove extension + path = [path stringByDeletingPathExtension]; + + // Remove scale suffix + path = [RCTAssetURLScaleRegex() stringByReplacingMatchesInString:path + options:0 + range:NSMakeRange(0, [path length]) + withTemplate:@""]; + + path = [path lowercaseString]; + + // Encode folder structure in file name + path = [path stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; + + // Remove illegal chars + path = [RCTAssetURLCharactersRegex() stringByReplacingMatchesInString:path + options:0 + range:NSMakeRange(0, [path length]) + withTemplate:@""]; + + // Remove "assets_" prefix + if ([path hasPrefix:@"assets_"]) { + path = [path substringFromIndex:@"assets_".length]; + } + + return path; +} + static BOOL RCTIsImageAssetsPath(NSString *path) { NSString *extension = [path pathExtension]; @@ -800,6 +856,17 @@ BOOL RCTIsLocalAssetURL(NSURL *__nullable imageURL) UIImage *__nullable RCTImageFromLocalAssetURL(NSURL *imageURL) { + NSString *catalogName = RCTAssetCatalogNameForURL(imageURL); + if (catalogName) { + UIImage *image = [UIImage imageNamed:catalogName]; + if (image) { + return image; + } else { + RCTLogWarn( + @"Image %@ not found in the asset catalog. Make sure your app template is updated correctly.", catalogName); + } + } + NSString *imageName = RCTBundlePathForURL(imageURL); NSBundle *bundle = nil; diff --git a/packages/react-native/scripts/react-native-xcode.sh b/packages/react-native/scripts/react-native-xcode.sh index c34ea666d78261..7aba639c6dc4c1 100755 --- a/packages/react-native/scripts/react-native-xcode.sh +++ b/packages/react-native/scripts/react-native-xcode.sh @@ -139,6 +139,10 @@ if [[ $USE_HERMES != false && $DEV == false ]]; then EXTRA_ARGS="$EXTRA_ARGS --minify false" fi +# PRODUCT_SETTINGS_PATH is where the target Info.plist file is. The asset +# catalog will be in the same folder. +ASSET_CATALOG_DEST=${ASSET_CATALOG_DEST:-"$(dirname "$PRODUCT_SETTINGS_PATH")"} + "$NODE_BINARY" $NODE_ARGS "$CLI_PATH" $BUNDLE_COMMAND \ $CONFIG_ARG \ --entry-file "$ENTRY_FILE" \ @@ -147,6 +151,7 @@ fi --reset-cache \ --bundle-output "$BUNDLE_FILE" \ --assets-dest "$DEST" \ + --asset-catalog-dest "$ASSET_CATALOG_DEST" \ $EXTRA_ARGS \ $EXTRA_PACKAGER_ARGS diff --git a/packages/react-native/template/_gitignore b/packages/react-native/template/_gitignore index 0cab2ac6fc46cd..e2bfd3b99d81d2 100644 --- a/packages/react-native/template/_gitignore +++ b/packages/react-native/template/_gitignore @@ -64,3 +64,6 @@ yarn-error.log # testing /coverage + +# Generated asset catalog +**/RNAssets.xcassets/*.imageset diff --git a/packages/react-native/template/ios/HelloWorld.xcodeproj/project.pbxproj b/packages/react-native/template/ios/HelloWorld.xcodeproj/project.pbxproj index eed43451e53eb5..f75bbfa7646de1 100644 --- a/packages/react-native/template/ios/HelloWorld.xcodeproj/project.pbxproj +++ b/packages/react-native/template/ios/HelloWorld.xcodeproj/project.pbxproj @@ -10,7 +10,8 @@ 00E356F31AD99517003FC87E /* HelloWorldTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* HelloWorldTests.m */; }; 0C80B921A6F3F58F76C31292 /* libPods-HelloWorld.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-HelloWorld.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; - 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 0C49AA4A252E5346004CE48B /* RNAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0C49AA49252E5346004CE48B /* RNAssets.xcassets */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 7699B88040F8A987B510C191 /* libPods-HelloWorld-HelloWorldTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-HelloWorld-HelloWorldTests.a */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; @@ -30,6 +31,7 @@ 00E356EE1AD99517003FC87E /* HelloWorldTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HelloWorldTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* HelloWorldTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HelloWorldTests.m; sourceTree = ""; }; + 0C49AA49252E5346004CE48B /* RNAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = RNAssets.xcassets; path = HelloWorld/RNAssets.xcassets; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* HelloWorld.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HelloWorld.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = HelloWorld/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = HelloWorld/AppDelegate.mm; sourceTree = ""; }; @@ -89,6 +91,7 @@ 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.mm */, 13B07FB51A68108700A75B9A /* Images.xcassets */, + 0C49AA49252E5346004CE48B /* RNAssets.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, @@ -179,8 +182,8 @@ C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, - 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 13B07F8E1A680F5B00A75B9A /* Resources */, 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, ); @@ -242,6 +245,7 @@ buildActionMask = 2147483647; files = ( 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 0C49AA4A252E5346004CE48B /* RNAssets.xcassets in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/packages/rn-tester/RNTester/RNAssets.xcassets/Contents.json b/packages/rn-tester/RNTester/RNAssets.xcassets/Contents.json new file mode 100644 index 00000000000000..73c00596a7fca3 --- /dev/null +++ b/packages/rn-tester/RNTester/RNAssets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 68261744016a63..e5540570bc64a9 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 04157F50C11E9F16DDD69B17 /* libPods-RNTester.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F98312BF816A7F2688C036D /* libPods-RNTester.a */; }; + 0CF641B628ECB21C00DCDD11 /* RNAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CF641B528ECB21C00DCDD11 /* RNAssets.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 2DDEF0101F84BF7B00DBDF73 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DDEF00F1F84BF7B00DBDF73 /* Images.xcassets */; }; 383889DA23A7398900D06C3E /* RCTConvert_UIColorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */; }; @@ -78,6 +79,7 @@ /* Begin PBXFileReference section */ 0CC3BE1A25DDB68A0033CAEB /* RNTester.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = RNTester.entitlements; path = RNTester/RNTester.entitlements; sourceTree = ""; }; + 0CF641B528ECB21C00DCDD11 /* RNAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = RNAssets.xcassets; path = RNTester/RNAssets.xcassets; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* RNTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RNTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = RNTester/AppDelegate.h; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RNTester/Info.plist; sourceTree = ""; }; @@ -218,6 +220,7 @@ 13B07FB71A68108700A75B9A /* main.m */, 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */, 2DDEF00F1F84BF7B00DBDF73 /* Images.xcassets */, + 0CF641B528ECB21C00DCDD11 /* RNAssets.xcassets */, 8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */, 680759612239798500290469 /* Fabric */, 272E6B3A1BEA846C001FCF37 /* NativeExampleViews */, @@ -377,8 +380,9 @@ 3B2555DA1E9C50DBD7DEDDC7 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, - 13B07F8E1A680F5B00A75B9A /* Resources */, 68CD48B71D2BCB2C007E06A9 /* Build JS Bundle */, + 5CF0FD27207FC6EC00C13D65 /* Start Metro */, + 13B07F8E1A680F5B00A75B9A /* Resources */, 79E8BE2B119D4C5CCD2F04B3 /* [RN] Copy Hermes Framework */, 4E5A5A192F46F13B14A915AF /* [CP] Embed Pods Frameworks */, 4E2AB2EE08A8E6F86E659152 /* [CP] Copy Pods Resources */, @@ -480,6 +484,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0CF641B628ECB21C00DCDD11 /* RNAssets.xcassets in Resources */, 2DDEF0101F84BF7B00DBDF73 /* Images.xcassets in Resources */, 8145AE06241172D900A3F8DA /* LaunchScreen.storyboard in Resources */, 3D2AFAF51D646CF80089D1A3 /* legacy_image@2x.png in Resources */, diff --git a/packages/rn-tester/RNTesterUnitTests/RCTURLUtilsTests.m b/packages/rn-tester/RNTesterUnitTests/RCTURLUtilsTests.m index 318ed489038141..b5dcb59ba1d820 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTURLUtilsTests.m +++ b/packages/rn-tester/RNTesterUnitTests/RCTURLUtilsTests.m @@ -100,4 +100,25 @@ - (void)testIsLocalAssetsURLParam XCTAssertFalse(RCTIsLocalAssetURL(otherAssetsURL)); } +- (void)testAssetCatalogNameForURL +{ + NSString *validAssetPath = + [NSString stringWithFormat:@"file://%@/assets/AwesomeModule/icon@2x.png", [[NSBundle mainBundle] resourcePath]]; + NSString *result = RCTAssetCatalogNameForURL([NSURL URLWithString:validAssetPath]); + XCTAssertEqualObjects(result, @"awesomemodule_icon"); + + NSString *validAssetNoScalePath = + [NSString stringWithFormat:@"file://%@/assets/AwesomeModule/icon.png", [[NSBundle mainBundle] resourcePath]]; + result = RCTAssetCatalogNameForURL([NSURL URLWithString:validAssetNoScalePath]); + XCTAssertEqualObjects(result, @"awesomemodule_icon"); + + NSString *notPackagerAssetPath = + [NSString stringWithFormat:@"file://%@/icon.png", [[NSBundle mainBundle] resourcePath]]; + result = RCTAssetCatalogNameForURL([NSURL URLWithString:notPackagerAssetPath]); + XCTAssertNil(result); + + result = RCTAssetCatalogNameForURL(nil); + XCTAssertNil(result); +} + @end diff --git a/template/ios/HelloWorld/RNAssets.xcassets/Contents.json b/template/ios/HelloWorld/RNAssets.xcassets/Contents.json new file mode 100644 index 00000000000000..73c00596a7fca3 --- /dev/null +++ b/template/ios/HelloWorld/RNAssets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +}